From 901fc8d760cbcc237fbc21283614e271f22ec192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Mon, 12 Apr 2021 17:41:42 +0200 Subject: [PATCH] YT: try harder to find suitable DASH streams Instead of searching for 1080p only, accept also other H.264 formats for DASH streaming --- src/dash.js | 31 +++------------ src/youtube.js | 102 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 90 insertions(+), 43 deletions(-) diff --git a/src/dash.js b/src/dash.js index 01a34809..cb57b1d6 100644 --- a/src/dash.js +++ b/src/dash.js @@ -37,7 +37,9 @@ function generateDash(dashInfo) function _addAdaptationSet(streamsArr) { - const mimeInfo = _getMimeInfo(streamsArr[0].mimeType); + /* We just need it for adaptation type, + * so any stream will do */ + const { mimeInfo } = streamsArr[0]; const adaptArr = [ `contentType="${mimeInfo.content}"`, @@ -93,11 +95,9 @@ function _addAdaptationSet(streamsArr) function _getStreamRepresentation(stream) { - const mimeInfo = _getMimeInfo(stream.mimeType); - const repOptsArr = [ `id="${stream.itag}"`, - `codecs="${mimeInfo.codecs}"`, + `codecs="${stream.mimeInfo.codecs}"`, `bandwidth="${stream.bitrate}"`, ]; @@ -120,13 +120,8 @@ function _getStreamRepresentation(stream) repArr.push(` `); } - const encodedURL = Misc.encodeHTML(stream.url) - .replace('?', '/') - .replace(/&/g, '/') - .replace(/=/g, '/'); - repArr.push( - ` ${encodedURL}` + ` ${stream.url}` ); if(stream.indexRange) { @@ -152,22 +147,6 @@ function _getStreamRepresentation(stream) return repArr.join('\n'); } -function _getMimeInfo(mimeType) -{ - const mimeArr = mimeType.split(';'); - - let codecs = mimeArr.find(info => info.includes('codecs')).split('=')[1]; - codecs = codecs.substring(1, codecs.length - 1); - - const mimeInfo = { - content: mimeArr[0].split('/')[0], - type: mimeArr[0], - codecs, - }; - - return mimeInfo; -} - function _getPar(width, height) { const gcd = _getGCD(width, height); diff --git a/src/youtube.js b/src/youtube.js index afc34a5d..c84b2010 100644 --- a/src/youtube.js +++ b/src/youtube.js @@ -357,27 +357,75 @@ var YouTubeClient = GObject.registerClass({ ) 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')); - }); + /* TODO: Options in prefs to set preferred video formats and adaptive streaming */ + const isAdaptiveEnabled = false; + const allowedFormats = { + video: [ + 133, + 134, + 135, + 136, + 137, + 298, + 299, + ], + audio: [ + 140, + ] + }; - if(!videoStream || !audioStream) - return null; + const filteredStreams = { + video: [], + audio: [], + }; + + for(let fmt of ['video', 'audio']) { + debug(`filtering ${fmt} streams`); + let index = allowedFormats[fmt].length; + + while(index--) { + const itag = allowedFormats[fmt][index]; + const foundStream = info.streamingData.adaptiveFormats.find(stream => (stream.itag == itag)); + if(foundStream) { + /* Parse and convert mimeType string into object */ + foundStream.mimeInfo = this._getMimeInfo(foundStream.mimeType); + + /* Sanity check */ + if(!foundStream.mimeInfo || foundStream.mimeInfo.content !== fmt) { + debug(new Error(`mimeType parsing failed on stream: ${itag}`)); + continue; + } + + /* Sort from worst to best */ + filteredStreams[fmt].unshift(foundStream); + debug(`added ${fmt} itag: ${foundStream.itag}`); + + if(!isAdaptiveEnabled) + break; + } + } + if(!filteredStreams[fmt].length) { + debug(`dash info ${fmt} streams list is empty`); + return null; + } + } debug('following redirects'); - for(let stream of [videoStream, audioStream]) { - debug(`initial URL: ${stream.url}`); + for(let fmtArr of Object.values(filteredStreams)) { + for(let stream of fmtArr) { + debug(`initial URL: ${stream.url}`); - const result = await this._downloadDataPromise(stream.url, 'HEAD').catch(debug); - if(!result) return null; + const result = await this._downloadDataPromise(stream.url, 'HEAD').catch(debug); + if(!result) return null; - stream.url = result.uri; - debug(`resolved URL: ${stream.url}`); + stream.url = Misc.encodeHTML(result.uri) + .replace('?', '/') + .replace(/&/g, '/') + .replace(/=/g, '/'); + + debug(`resolved URL: ${stream.url}`); + } } debug('all redirects resolved'); @@ -385,8 +433,8 @@ var YouTubeClient = GObject.registerClass({ return { duration: info.videoDetails.lengthSeconds, adaptations: [ - [videoStream], - [audioStream], + filteredStreams.video, + filteredStreams.audio, ] }; } @@ -527,6 +575,26 @@ var YouTubeClient = GObject.registerClass({ return reduced; } + _getMimeInfo(mimeType) + { + debug(`parsing mimeType: ${mimeType}`); + + const mimeArr = mimeType.split(';'); + + let codecs = mimeArr.find(info => info.includes('codecs')).split('=')[1]; + codecs = codecs.substring(1, codecs.length - 1); + + const mimeInfo = { + content: mimeArr[0].split('/')[0], + type: mimeArr[0], + codecs, + }; + + debug(`parsed mimeType: ${JSON.stringify(mimeInfo)}`); + + return mimeInfo; + } + _getPlayerInfoPromise(videoId) { const data = this._getPlayerPostData(videoId);