From ab8cafa0b83f947f4a46861acccf32ee3cebe004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sat, 17 Apr 2021 18:02:33 +0200 Subject: [PATCH] YT: support non-adaptive live streaming --- src/youtube.js | 113 ++++++++++++++++++++++++++++---------------- src/youtubeItags.js | 19 ++++++-- 2 files changed, 88 insertions(+), 44 deletions(-) diff --git a/src/youtube.js b/src/youtube.js index 63ea0e9d..c7f9660a 100644 --- a/src/youtube.js +++ b/src/youtube.js @@ -320,7 +320,7 @@ var YouTubeClient = GObject.registerClass({ type: settings.get_string('yt-quality-type'), }; - uri = this.getHLSUri(info, itagOpts); + uri = await this.getHLSUriAsync(info, itagOpts); if(!uri) { const dashInfo = await this.getDashInfoAsync(info, itagOpts).catch(debug); @@ -361,6 +361,55 @@ var YouTubeClient = GObject.registerClass({ return { uri, title }; } + async getHLSUriAsync(info, itagOpts) + { + const isLive = info.videoDetails.isLiveContent; + debug(`video is live: ${isLive}`); + + /* YouTube only uses HLS for live content */ + if(!isLive) + return null; + + const hlsUri = info.streamingData.hlsManifestUrl; + if(!hlsUri) { + debug(new Error('no HLS manifest URL')); + return null; + } + + if(!settings.get_boolean('yt-adaptive-enabled')) { + const result = await this._downloadDataPromise(hlsUri).catch(debug); + if(!result || !result.data) { + debug(new Error('HLS manifest download failed')); + return hlsUri; + } + + const hlsArr = result.data.split('\n'); + const hlsStreams = []; + + let index = hlsArr.length; + while(index--) { + const url = hlsArr[index]; + if(!Gst.Uri.is_valid(url)) + continue; + + const itagIndex = url.indexOf('/itag/') + 6; + const itag = url.substring(itagIndex, itagIndex + 2); + + hlsStreams.push({ itag, url }); + } + + debug(`obtaining HLS itags for resolution: ${itagOpts.width}x${itagOpts.height}`); + const hlsItags = YTItags.getHLSItags(itagOpts); + debug(`HLS itags: ${JSON.stringify(hlsItags)}`); + + const hlsStream = this.getBestStreamFromItags(hlsStreams, hlsItags); + if(hlsStream) + return hlsStream.url; + } + + return hlsUri; + } + async getDashInfoAsync(info, itagOpts) { if( @@ -373,9 +422,9 @@ var YouTubeClient = GObject.registerClass({ /* TODO: Options in prefs to set preferred video formats and adaptive streaming */ const isAdaptiveEnabled = settings.get_boolean('yt-adaptive-enabled'); - debug(`obtaining dash itags for resolution: ${itagOpts.width}x${itagOpts.height}`); + debug(`obtaining DASH itags for resolution: ${itagOpts.width}x${itagOpts.height}`); const dashItags = YTItags.getDashItags(itagOpts); - debug(`dash itags: ${JSON.stringify(dashItags)}`); + debug(`DASH itags: ${JSON.stringify(dashItags)}`); const filteredStreams = { video: [], @@ -442,53 +491,20 @@ var YouTubeClient = GObject.registerClass({ }; } - getHLSUri(info, itagOpts) - { - const isLive = info.videoDetails.isLiveContent; - debug(`video is live: ${isLive}`); - - /* YouTube only uses HLS for live content */ - if(!isLive) - return null; - - const hlsUri = info.streamingData.hlsManifestUrl; - if(!hlsUri) { - debug(new Error('no HLS manifest URL')); - return null; - } - - /* TODO: download manifest and select best resolution - * for monitor when adaptive streaming is disabled */ - - return hlsUri; - } - getBestCombinedUri(info, itagOpts) { debug(`obtaining best combined URL for resolution: ${itagOpts.width}x${itagOpts.height}`); + const streams = info.streamingData.formats; - if(!info.streamingData.formats.length) + if(!streams.length) return null; - let combinedStream; - const combinedItags = YTItags.getCombinedItags(itagOpts); - let index = combinedItags.length; - - while(index--) { - const itag = combinedItags[index]; - combinedStream = info.streamingData.formats.find(stream => stream.itag == itag); - if(combinedStream) { - debug(`found best combined itag: ${combinedStream.itag}`); - break; - } - } + let combinedStream = this.getBestStreamFromItags(streams, combinedItags); if(!combinedStream) { debug('trying any combined stream as last resort'); - combinedStream = info.streamingData.formats[ - info.streamingData.formats.length - 1 - ]; + combinedStream = streams[streams.length - 1]; } if(!combinedStream || !combinedStream.url) @@ -497,6 +513,23 @@ var YouTubeClient = GObject.registerClass({ return combinedStream.url; } + getBestStreamFromItags(streams, itags) + { + let index = itags.length; + + while(index--) { + const itag = itags[index]; + const stream = streams.find(stream => stream.itag == itag); + if(stream) { + debug(`found preferred stream itag: ${stream.itag}`); + return stream; + } + } + debug('could not find preferred stream for itags'); + + return null; + } + compareLastVideoId(videoId) { if(!this.lastInfo) diff --git a/src/youtubeItags.js b/src/youtubeItags.js index d4c1545d..374ea8a1 100644 --- a/src/youtubeItags.js +++ b/src/youtubeItags.js @@ -21,6 +21,13 @@ const Itags = { combined: { 360: 18, 720: 22, + }, + hls: { + 240: 92, + 360: 93, + 480: 94, + 720: 95, + 1080: 96, } }; @@ -37,6 +44,8 @@ function _appendItagArray(arr, opts, formats) ) break; } + + return arr; } function getDashItags(opts) @@ -62,8 +71,10 @@ function getDashItags(opts) function getCombinedItags(opts) { - const arr = []; - _appendItagArray(arr, opts, Itags.combined); - - return arr; + return _appendItagArray([], opts, Itags.combined); +} + +function getHLSItags(opts) +{ + return _appendItagArray([], opts, Itags.hls); }