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); }