Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • videostreaming/plugins/youtube
1 result
Show changes
Commits on Source (4)
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
"repositoryUrl": "https://futo.org", "repositoryUrl": "https://futo.org",
"scriptUrl": "./YoutubeScript.js", "scriptUrl": "./YoutubeScript.js",
"version": 231, "version": 232,
"iconUrl": "./youtube.png", "iconUrl": "./youtube.png",
"id": "35ae969a-a7db-11ed-afa1-0242ac120002", "id": "35ae969a-a7db-11ed-afa1-0242ac120002",
...@@ -107,6 +107,13 @@ ...@@ -107,6 +107,13 @@
"type": "Boolean", "type": "Boolean",
"default": "true" "default": "true"
}, },
{
"variable": "verifyIOSPlayback",
"name": "Verify IOS Playback",
"description": "This feature will check if iOS sources are playable beyond 60 seconds, or else fallback to other sources.\nMay add a minor delay on video load (< 0.2s)",
"type": "Boolean",
"default": "true"
},
{ {
"variable": "showVerboseToasts", "variable": "showVerboseToasts",
"name": "Show Verbose Messages", "name": "Show Verbose Messages",
...@@ -275,10 +282,17 @@ ...@@ -275,10 +282,17 @@
"changelog": { "changelog": {
"232": [
"Feature: Verify IOS Playback setting (true by default), checks if iOS sources can be played beyond 60 seconds, or fallback to others.",
"Feature: Channel shorts fetching support (may require app update to view new channel tab)"
],
"231": [
"Fix: UMP failing on initial load"
],
"227": [ "227": [
"UMP MP4 support (fixes no audio or only 360p for older videos)", "Feature: Opt-in AV1 support",
"UMP Disconnect toasts now optional", "Feature: UMP MP4 support (fixes no audio or only 360p for older videos)",
"Opt-in AV1 support" "Improve: UMP Disconnect toasts now optional"
] ]
} }
} }
...@@ -8,6 +8,7 @@ const URL_CONTEXT_M = "https://m.youtube.com"; ...@@ -8,6 +8,7 @@ const URL_CONTEXT_M = "https://m.youtube.com";
const URL_CHANNEL_VIDEOS = "/videos"; const URL_CHANNEL_VIDEOS = "/videos";
const URL_CHANNEL_STREAMS = "/streams"; const URL_CHANNEL_STREAMS = "/streams";
const URL_CHANNEL_PLAYLISTS = "/playlists"; const URL_CHANNEL_PLAYLISTS = "/playlists";
const URL_CHANNEL_SHORTS = "/shorts";
const URL_SEARCH_SUGGESTIONS = "https://suggestqueries-clients6.youtube.com/complete/search?client=youtube&gs_ri=youtube&ds=yt&q="; const URL_SEARCH_SUGGESTIONS = "https://suggestqueries-clients6.youtube.com/complete/search?client=youtube&gs_ri=youtube&ds=yt&q=";
const URL_SEARCH = "https://www.youtube.com/youtubei/v1/search"; const URL_SEARCH = "https://www.youtube.com/youtubei/v1/search";
const URL_BROWSE = "https://www.youtube.com/youtubei/v1/browse"; const URL_BROWSE = "https://www.youtube.com/youtubei/v1/browse";
...@@ -126,6 +127,8 @@ var _prefetchHomeUsed = false; ...@@ -126,6 +127,8 @@ var _prefetchHomeUsed = false;
const rootWindow = this; const rootWindow = this;
const canDoRequestWithBody = !!http.requestWithBody;
function getClientContext(isAuth = false) { function getClientContext(isAuth = false) {
return (isAuth) ? _clientContextAuth : _clientContext; return (isAuth) ? _clientContextAuth : _clientContext;
...@@ -147,6 +150,9 @@ source.enable = (conf, settings, saveStateStr) => { ...@@ -147,6 +150,9 @@ source.enable = (conf, settings, saveStateStr) => {
config = conf ?? {}; config = conf ?? {};
_settings = settings ?? {}; _settings = settings ?? {};
const codecs = (bridge.getHardwareCodecs) ? bridge.getHardwareCodecs() : [];
log(JSON.stringify(codecs));
USE_ABR_VIDEOS = !!_settings.useUMP && (bridge.buildSpecVersion ?? 1) > 1; USE_ABR_VIDEOS = !!_settings.useUMP && (bridge.buildSpecVersion ?? 1) > 1;
log("ABR Enabled: " + USE_ABR_VIDEOS); log("ABR Enabled: " + USE_ABR_VIDEOS);
...@@ -603,7 +609,15 @@ else { ...@@ -603,7 +609,15 @@ else {
if(!!_settings["showVerboseToasts"]) if(!!_settings["showVerboseToasts"])
bridge.toast("Using iOS sources fallback (" + (batchIOS > 0 ? "cached" : "lazily") + ")"); bridge.toast("Using iOS sources fallback (" + (batchIOS > 0 ? "cached" : "lazily") + ")");
let newDescriptor = extractAdaptiveFormats_VideoDescriptor(iosData.streamingData.adaptiveFormats, jsUrl, creationData, "IOS "); let newDescriptor = extractAdaptiveFormats_VideoDescriptor(iosData.streamingData.adaptiveFormats, jsUrl, creationData, "IOS ");
videoDetails.video = newDescriptor;
if(!!_settings.verifyIOSPlayback && !canDoRequestWithBody) {
log("Not doing verifyIOSPlayback because canDoRequestWithBody false");
}
if(!canDoRequestWithBody || !_settings.verifyIOSPlayback || verifyIOSPlayback(newDescriptor))
videoDetails.video = newDescriptor;
else {
log("IOS PLAYBACK VERIFICATION FAILED, FALLBACK");
}
} }
else { else {
log("Failed to get iOS stream data, fallback to UMP") log("Failed to get iOS stream data, fallback to UMP")
...@@ -1311,7 +1325,7 @@ source.getChannel = (url) => { ...@@ -1311,7 +1325,7 @@ source.getChannel = (url) => {
source.getChannelCapabilities = () => { source.getChannelCapabilities = () => {
return { return {
types: (!!_settings?.channelRssOnly) ? [Type.Feed.Mixed] : [Type.Feed.Videos, Type.Feed.Streams], types: (!!_settings?.channelRssOnly) ? [Type.Feed.Mixed] : [Type.Feed.Videos, Type.Feed.Streams, Type.Feed.Shorts],
sorts: (!!_settings?.channelRssOnly) ? [Type.Order.Chronological] : [Type.Order.Chronological, "Popular"]// sorts: (!!_settings?.channelRssOnly) ? [Type.Order.Chronological] : [Type.Order.Chronological, "Popular"]//
}; };
} }
...@@ -1362,6 +1376,10 @@ source.getChannelContents = (url, type, order, filters) => { ...@@ -1362,6 +1376,10 @@ source.getChannelContents = (url, type, order, filters) => {
targetTab = "Home"; targetTab = "Home";
url = url; url = url;
break; break;
case Type.Feed.Shorts:
targetTab = "Shorts";
url = url + URL_CHANNEL_SHORTS;
break;
default: default:
throw new ScriptException("Unsupported type: " + type); throw new ScriptException("Unsupported type: " + type);
} }
...@@ -1381,7 +1399,8 @@ source.getChannelContents = (url, type, order, filters) => { ...@@ -1381,7 +1399,8 @@ source.getChannelContents = (url, type, order, filters) => {
const channel = extractChannel_PlatformChannel(initialData, url); const channel = extractChannel_PlatformChannel(initialData, url);
const contextData = { const contextData = {
authorLink: new PlatformAuthorLink(new PlatformID(PLATFORM, channel.id.value, config.id, PLATFORM_CLAIMTYPE), channel.name, channel.url, channel.thumbnail) authorLink: new PlatformAuthorLink(new PlatformID(PLATFORM, channel.id.value, config.id, PLATFORM_CLAIMTYPE), channel.name, channel.url, channel.thumbnail),
allowShorts: type == Type.Feed.Shorts
}; };
const tabs = extractPage_Tabs(initialData, contextData); const tabs = extractPage_Tabs(initialData, contextData);
const tab = tabs.find(x=>x.title == targetTab); const tab = tabs.find(x=>x.title == targetTab);
...@@ -2409,12 +2428,13 @@ class YTABRExecutor { ...@@ -2409,12 +2428,13 @@ class YTABRExecutor {
key = parseInt(key); key = parseInt(key);
if(key < index || key > index + 7) { if(key < index || key > index + 7) {
log("UMP [" + this.type + "]: disposing segment " + key + " (<" + index + " || >" + (index + 6) + ")"); log("UMP [" + this.type + "]: disposing segment " + key + " (<" + index + " || >" + (index + 6) + ", total: " + Object.keys(this.segments).length + ")");
reusable?.free(this.segments[key].data); reusable?.free(this.segments[key].data);
const segment = this.segments[key]; const segment = this.segments[key];
if(segment) { if(segment) {
delete segment.data; delete segment.data;
} }
this.segments[key] = undefined;
delete this.segments[key]; delete this.segments[key];
} }
} }
...@@ -2423,9 +2443,11 @@ class YTABRExecutor { ...@@ -2423,9 +2443,11 @@ class YTABRExecutor {
const reusable = this.reusableBuffer; const reusable = this.reusableBuffer;
for(let key of Object.keys(this.segments)) { for(let key of Object.keys(this.segments)) {
reusable?.free(this.segments[key].data); reusable?.free(this.segments[key].data);
const buffer = this.segments[key]?.data?.buffer; const segment = this.segments[key];
if(buffer && !buffer.detached) const buffer = segment?.data?.buffer;
buffer?.transfer(); if(segment)
delete segment.data;
this.segments[key] = undefined;
delete this.segments[key]; delete this.segments[key];
} }
} }
...@@ -2530,10 +2552,17 @@ class YTABRExecutor { ...@@ -2530,10 +2552,17 @@ class YTABRExecutor {
const data = initialResp.body; const data = initialResp.body;
let byteArray = undefined; let byteArray = undefined;
if(data instanceof ArrayBuffer) let bufferToDispose = undefined;
if(data instanceof ArrayBuffer) {
log("Using ArrayBuffer!");
byteArray = new Uint8Array(data); byteArray = new Uint8Array(data);
else if(data instanceof Int8Array) bufferToDispose = data;
}
else if(data instanceof Int8Array) {
log("Using Uint8Array!");
byteArray = new Uint8Array(data.buffer); byteArray = new Uint8Array(data.buffer);
bufferToDispose = data.buffer;
}
else if(typeof data == "string") else if(typeof data == "string")
byteArray = Uint8Array.from(atob(data), c => c.charCodeAt(0)) byteArray = Uint8Array.from(atob(data), c => c.charCodeAt(0))
else { else {
...@@ -2551,8 +2580,9 @@ class YTABRExecutor { ...@@ -2551,8 +2580,9 @@ class YTABRExecutor {
const stream = umpResp.streams[key] const stream = umpResp.streams[key]
if(stream.itag == this.itag && stream.segmentIndex >= segment) if(stream.itag == this.itag && stream.segmentIndex >= segment)
streamsArr.push(stream); streamsArr.push(stream);
else else {
log(`IGNORING itag:${stream.itag}, segmentIndex: ${stream.segmentIndex}, segmentLength: ${stream.segmentSize}, completed: ${stream.completed}`) log(`IGNORING itag:${stream.itag}, segmentIndex: ${stream.segmentIndex}, segmentLength: ${stream.segmentSize}, completed: ${stream.completed}`);
}
} }
log("UMP [" + this.type + "] stream resps: \n" + streamsArr log("UMP [" + this.type + "] stream resps: \n" + streamsArr
.map(x=>`itag:${x.itag}, segmentIndex: ${x.segmentIndex}, segmentLength: ${x.segmentSize}, completed: ${x.completed}`) .map(x=>`itag:${x.itag}, segmentIndex: ${x.segmentIndex}, segmentLength: ${x.segmentSize}, completed: ${x.completed}`)
...@@ -3504,6 +3534,58 @@ function requestIOSStreamingData(videoId, batch, visitorData, useLogin) { ...@@ -3504,6 +3534,58 @@ function requestIOSStreamingData(videoId, batch, visitorData, useLogin) {
return resp; return resp;
} }
} }
function verifyIOSPlayback(descriptor) {
const startTime = new Date();
if (!descriptor) {
log("verifyIOSPlayback failed due to no descriptor");
return false;
}
if (!descriptor.audioSources) {
log("verifyIOSPlayback failed due to no descriptor");
return false;
}
if (descriptor.audioSources.length == 0) {
log("verifyIOSPlayback failed due to no audio streams");
return false;
}
const sourceToTest = descriptor.audioSources[0];
const modifier = sourceToTest.getRequestModifier();
console.log(modifier);
const modified = modifier.modifyRequest(sourceToTest.url, {
});
const resp1 = http.request("HEAD", modified.url, modified.headers, false);
if (!resp1.isOk) {
log("verifyIOSPlayback failed due couldn't determine content lenght with HEAD");
return false;
}
let contentLength = (resp1.headers["content-length"]) ? resp1.headers["content-length"][0] : -1;
if(contentLength && contentLength <= 0) {
log("verifyIOSPlayback failed due couldn't determine content lenght with HEAD (missing header)\n" + JSON.stringify(resp1));
return false;
}
const toTestRange = "bytes=" + (contentLength - 150) + "-" + (contentLength - 50);
const modified2 = modifier.modifyRequest(sourceToTest.url, {
"accept-ranges": "bytes",
"range": toTestRange
});
const resp2 = http.GET(modified2.url, modified2.headers, false);
console.log(resp2);
if (!resp2.isOk) {
log("verifyIOSPlayback failed due couldn't get segment beyond 60 seconds");
return false;
}
const timeToCheck = (new Date()).getTime() - startTime.getTime();
log("verifyIOSPlayback succeeded in " + timeToCheck + "ms");
return true;
}
function requestAndroidStreamingData(videoId) { function requestAndroidStreamingData(videoId) {
const body = { const body = {
videoId: videoId, videoId: videoId,
...@@ -3963,8 +4045,8 @@ function extractVideoPage_VideoDetails(parentUrl, initialData, initialPlayerData ...@@ -3963,8 +4045,8 @@ function extractVideoPage_VideoDetails(parentUrl, initialData, initialPlayerData
dash: (videoDetails?.isLive ?? false) ? dashSource : null, dash: (videoDetails?.isLive ?? false) ? dashSource : null,
live: (videoDetails?.isLive ?? false) ? (hlsSource ?? dashSource) : null, live: (videoDetails?.isLive ?? false) ? (hlsSource ?? dashSource) : null,
video: video:
((!useAbr) ? (//(!useAbr) ?
extractAdaptiveFormats_VideoDescriptor(initialPlayerData?.streamingData?.adaptiveFormats, jsUrl, contextData, "") : //extractAdaptiveFormats_VideoDescriptor(initialPlayerData?.streamingData?.adaptiveFormats, jsUrl, contextData, "") :
extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clientConfig, parentUrl, usedLogin) extractABR_VideoDescriptor(initialPlayerData, jsUrl, initialData, clientConfig, parentUrl, usedLogin)
) )
?? new VideoSourceDescriptor([]), ?? new VideoSourceDescriptor([]),
...@@ -4812,6 +4894,12 @@ function switchKeyVideo(content, contextData) { ...@@ -4812,6 +4894,12 @@ function switchKeyVideo(content, contextData) {
adSlotRenderer(adSlot) { adSlotRenderer(adSlot) {
return null; return null;
}, },
shortsLockupViewModel(renderer) {
if(contextData?.allowShorts)
return extractShortLockupViewModel_Video(renderer, contextData);
else
return null;
},
default(name) { default(name) {
return null; return null;
} }
...@@ -5004,6 +5092,35 @@ function extractVideoRenderer_Video(videoRenderer, contextData) { ...@@ -5004,6 +5092,35 @@ function extractVideoRenderer_Video(videoRenderer, contextData) {
extractType: "Video" extractType: "Video"
}); });
} }
function extractShortLockupViewModel_Video(videoRenderer, contextData) {
if(!contextData || !contextData.authorLink)
return null;
const author = (contextData && contextData.authorLink) ?
contextData.authorLink : extractVideoRenderer_AuthorLink(videoRenderer);
if(IS_TESTING)
console.log(videoRenderer);
const id = videoRenderer?.onTap?.innertubeCommand?.reelWatchEndpoint?.videoId;
if(!id)
return null;
return new PlatformVideo({
id: new PlatformID(PLATFORM, id, config.id),
name: escapeUnicode(extractText_String(videoRenderer.overlayMetadata.primaryText.content)),
thumbnails: extractThumbnail_Thumbnails(videoRenderer.thumbnail),
author: author,
uploadDate: undefined,//parseInt(extractAgoText_Timestamp(videoRenderer.publishedTimeText.simpleText)),
duration: 0, //extractHumanTime_Seconds(videoRenderer.lengthText.simpleText),
viewCount: extractFirstNumber_Integer(extractText_String(videoRenderer.overlayMetadata.secondaryText)),
url: URL_BASE + "/watch?v=" + id,
isLive: false,
extractType: "Video",
isShort: true
});
}
function extractReelItemRenderer_Video(reelItemRenderer) { function extractReelItemRenderer_Video(reelItemRenderer) {
//We don't do shorts for now.. //We don't do shorts for now..
return null; return null;
...@@ -5127,7 +5244,12 @@ function extractRuns_AuthorLink(runs) { ...@@ -5127,7 +5244,12 @@ function extractRuns_AuthorLink(runs) {
} }
function extractThumbnail_Thumbnails(thumbnail) { function extractThumbnail_Thumbnails(thumbnail) {
return new Thumbnails(thumbnail.thumbnails.map(x=>new Thumbnail(escapeUnicode(x.url), x.height))); if(thumbnail.thumbnails)
return new Thumbnails(thumbnail.thumbnails.map(x=>new Thumbnail(escapeUnicode(x.url), x.height)));
else if(thumbnail.sources)
return new Thumbnails(thumbnail.sources.map(x=>new Thumbnail(escapeUnicode(x.url), x.height)));
else
return new Thumbnails([]);
} }
function extractThumbnail_BestUrl(thumbnail) { function extractThumbnail_BestUrl(thumbnail) {
if(!thumbnail?.thumbnails || thumbnail.thumbnails.length <= 0) if(!thumbnail?.thumbnails || thumbnail.thumbnails.length <= 0)
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
"repositoryUrl": "https://futo.org", "repositoryUrl": "https://futo.org",
"scriptUrl": "./YoutubeScript.js", "scriptUrl": "./YoutubeScript.js",
"version": 231, "version": 232,
"iconUrl": "./youtube.png", "iconUrl": "./youtube.png",
"id": "35ae969a-a7db-11ed-afa1-0242ac120003", "id": "35ae969a-a7db-11ed-afa1-0242ac120003",
...@@ -107,6 +107,13 @@ ...@@ -107,6 +107,13 @@
"type": "Boolean", "type": "Boolean",
"default": "true" "default": "true"
}, },
{
"variable": "verifyIOSPlayback",
"name": "Verify IOS Playback",
"description": "This feature will check if iOS sources are playable beyond 60 seconds, or else fallback to other sources.\nMay add a minor delay on video load (< 0.2s)",
"type": "Boolean",
"default": "true"
},
{ {
"variable": "showVerboseToasts", "variable": "showVerboseToasts",
"name": "Show Verbose Messages", "name": "Show Verbose Messages",
......