From 7cf86e92eba84945c45df03b6d6274f96bd0d951 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Sun, 11 Apr 2021 14:46:08 +0200 Subject: [PATCH] YT: resolve redirects on the Clapper side Instead of providing URIs directly to GStreamer, follow redirects and provide that final URI. With this change souphttpsrc will not have to go through redirects from the beginning for each video segment. --- src/dash.js | 38 ++++++++++--------------------- src/player.js | 21 +++++++++++------ src/youtube.js | 61 +++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 84 insertions(+), 36 deletions(-) diff --git a/src/dash.js b/src/dash.js index 4e715412..01a34809 100644 --- a/src/dash.js +++ b/src/dash.js @@ -4,29 +4,11 @@ const Misc = imports.src.misc; const { debug } = Debug; -function generateDash(info) +function generateDash(dashInfo) { - if( - !info.streamingData - || !info.streamingData.adaptiveFormats - || !info.streamingData.adaptiveFormats.length - ) - return null; - debug('generating dash'); - /* TODO: Options in prefs to set preferred video formats for adaptive streaming */ - const videoStream = info.streamingData.adaptiveFormats.find(stream => { - return (stream.mimeType.startsWith('video/mp4') && stream.quality === 'hd1080'); - }); - const audioStream = info.streamingData.adaptiveFormats.find(stream => { - return (stream.mimeType.startsWith('audio/mp4')); - }); - - if(!videoStream || !audioStream) - return null; - - const bufferSec = Math.min(4, info.videoDetails.lengthSeconds); + const bufferSec = Math.min(4, dashInfo.duration); const dash = [ ``, @@ -34,19 +16,23 @@ function generateDash(info) ` xmlns="urn:mpeg:dash:schema:mpd:2011"`, ` xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd"`, ` type="static"`, - ` mediaPresentationDuration="PT${info.videoDetails.lengthSeconds}S"`, + ` mediaPresentationDuration="PT${dashInfo.duration}S"`, ` minBufferTime="PT${bufferSec}S"`, ` profiles="urn:mpeg:dash:profile:isoff-on-demand:2011">`, - ` `, - _addAdaptationSet([videoStream]), - _addAdaptationSet([audioStream]), + ` ` + ]; + + for(let adaptation of dashInfo.adaptations) + dash.push(_addAdaptationSet(adaptation)); + + dash.push( ` `, `` - ].join('\n'); + ); debug('dash generated'); - return dash; + return dash.join('\n'); } function _addAdaptationSet(streamsArr) diff --git a/src/player.js b/src/player.js index fe70205e..7a80ca56 100644 --- a/src/player.js +++ b/src/player.js @@ -86,16 +86,23 @@ class ClapperPlayer extends PlayerBase if(!info) throw new Error('no YouTube video info'); - const dash = Dash.generateDash(info); let videoUri = null; + const dashInfo = await this.ytClient.getDashInfoAsync(info).catch(debug); - if(dash) { - const dashFile = await FileOps.saveFilePromise( - 'tmp', null, 'clapper.mpd', dash - ).catch(debug); + if(dashInfo) { + debug('parsed video info to dash info'); + const dash = Dash.generateDash(dashInfo); - if(dashFile) - videoUri = dashFile.get_uri(); + if(dash) { + debug('got dash'); + + const dashFile = await FileOps.saveFilePromise( + 'tmp', null, 'clapper.mpd', dash + ).catch(debug); + + if(dashFile) + videoUri = dashFile.get_uri(); + } } if(!videoUri) diff --git a/src/youtube.js b/src/youtube.js index 8cb5650c..5aa7a03e 100644 --- a/src/youtube.js +++ b/src/youtube.js @@ -23,7 +23,7 @@ var YouTubeClient = GObject.registerClass({ _init() { super._init({ - timeout: 5, + timeout: 7, max_conns_per_host: 1, /* TODO: share this with GstClapper lib (define only once) */ user_agent: 'Mozilla/5.0 (X11; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0', @@ -190,7 +190,10 @@ var YouTubeClient = GObject.registerClass({ debug(`found player URI: ${ytUri}`); const ytId = ytPath.split('/').find(el => Misc.isHex(el)); - let ytSigData = await FileOps.getFileContentsPromise('user_cache', 'yt-sig', ytId).catch(debug); + let ytSigData = await FileOps.getFileContentsPromise( + 'user_cache', 'yt-sig', ytId + ).catch(debug); + if(ytSigData) { ytSigData = ytSigData.split(';'); @@ -299,8 +302,53 @@ var YouTubeClient = GObject.registerClass({ }); } + async getDashInfoAsync(info) + { + if( + !info.streamingData + || !info.streamingData.adaptiveFormats + || !info.streamingData.adaptiveFormats.length + ) + return null; + + /* TODO: Options in prefs to set preferred video formats for adaptive streaming */ + const videoStream = info.streamingData.adaptiveFormats.find(stream => { + return (stream.mimeType.startsWith('video/mp4') && stream.quality === 'hd1080'); + }); + const audioStream = info.streamingData.adaptiveFormats.find(stream => { + return (stream.mimeType.startsWith('audio/mp4')); + }); + + if(!videoStream || !audioStream) + return null; + + debug('following redirects'); + + for(let stream of [videoStream, audioStream]) { + debug(`initial URL: ${stream.url}`); + + const result = await this._downloadDataPromise(stream.url, 'HEAD').catch(debug); + if(!result) return null; + + stream.url = result.uri; + debug(`resolved URL: ${stream.url}`); + } + + debug('all redirects resolved'); + + return { + duration: info.videoDetails.lengthSeconds, + adaptations: [ + [videoStream], + [audioStream], + ] + }; + } + getBestCombinedUri(info) { + debug('obtaining best combined URL'); + if(!info.streamingData.formats.length) return null; @@ -332,11 +380,14 @@ var YouTubeClient = GObject.registerClass({ _downloadDataPromise(url, method, reqData) { + method = method || 'GET'; + return new Promise((resolve, reject) => { - const message = Soup.Message.new(method || 'GET', url); + const message = Soup.Message.new(method, url); const result = { data: null, isAborted: false, + uri: null, }; if(reqData) { @@ -353,6 +404,10 @@ var YouTubeClient = GObject.registerClass({ if(statusCode === 200) { result.data = msg.response_body.data; + + if(method === 'HEAD') + result.uri = msg.uri.to_string(false); + return resolve(result); }