YT: try harder to find suitable DASH streams

Instead of searching for 1080p only, accept also other H.264 formats for DASH streaming
This commit is contained in:
Rafał Dzięgiel
2021-04-12 17:41:42 +02:00
parent ab32b2dbbc
commit 901fc8d760
2 changed files with 90 additions and 43 deletions

View File

@@ -37,7 +37,9 @@ function generateDash(dashInfo)
function _addAdaptationSet(streamsArr) 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 = [ const adaptArr = [
`contentType="${mimeInfo.content}"`, `contentType="${mimeInfo.content}"`,
@@ -93,11 +95,9 @@ function _addAdaptationSet(streamsArr)
function _getStreamRepresentation(stream) function _getStreamRepresentation(stream)
{ {
const mimeInfo = _getMimeInfo(stream.mimeType);
const repOptsArr = [ const repOptsArr = [
`id="${stream.itag}"`, `id="${stream.itag}"`,
`codecs="${mimeInfo.codecs}"`, `codecs="${stream.mimeInfo.codecs}"`,
`bandwidth="${stream.bitrate}"`, `bandwidth="${stream.bitrate}"`,
]; ];
@@ -120,13 +120,8 @@ function _getStreamRepresentation(stream)
repArr.push(` <AudioChannelConfiguration ${audioConfArr.join(' ')}/>`); repArr.push(` <AudioChannelConfiguration ${audioConfArr.join(' ')}/>`);
} }
const encodedURL = Misc.encodeHTML(stream.url)
.replace('?', '/')
.replace(/&amp;/g, '/')
.replace(/=/g, '/');
repArr.push( repArr.push(
` <BaseURL>${encodedURL}</BaseURL>` ` <BaseURL>${stream.url}</BaseURL>`
); );
if(stream.indexRange) { if(stream.indexRange) {
@@ -152,22 +147,6 @@ function _getStreamRepresentation(stream)
return repArr.join('\n'); 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) function _getPar(width, height)
{ {
const gcd = _getGCD(width, height); const gcd = _getGCD(width, height);

View File

@@ -357,27 +357,75 @@ var YouTubeClient = GObject.registerClass({
) )
return null; return null;
/* TODO: Options in prefs to set preferred video formats for adaptive streaming */ /* TODO: Options in prefs to set preferred video formats and adaptive streaming */
const videoStream = info.streamingData.adaptiveFormats.find(stream => { const isAdaptiveEnabled = false;
return (stream.mimeType.startsWith('video/mp4') && stream.quality === 'hd1080'); const allowedFormats = {
}); video: [
const audioStream = info.streamingData.adaptiveFormats.find(stream => { 133,
return (stream.mimeType.startsWith('audio/mp4')); 134,
}); 135,
136,
137,
298,
299,
],
audio: [
140,
]
};
if(!videoStream || !audioStream) const filteredStreams = {
return null; 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'); debug('following redirects');
for(let stream of [videoStream, audioStream]) { for(let fmtArr of Object.values(filteredStreams)) {
debug(`initial URL: ${stream.url}`); for(let stream of fmtArr) {
debug(`initial URL: ${stream.url}`);
const result = await this._downloadDataPromise(stream.url, 'HEAD').catch(debug); const result = await this._downloadDataPromise(stream.url, 'HEAD').catch(debug);
if(!result) return null; if(!result) return null;
stream.url = result.uri; stream.url = Misc.encodeHTML(result.uri)
debug(`resolved URL: ${stream.url}`); .replace('?', '/')
.replace(/&amp;/g, '/')
.replace(/=/g, '/');
debug(`resolved URL: ${stream.url}`);
}
} }
debug('all redirects resolved'); debug('all redirects resolved');
@@ -385,8 +433,8 @@ var YouTubeClient = GObject.registerClass({
return { return {
duration: info.videoDetails.lengthSeconds, duration: info.videoDetails.lengthSeconds,
adaptations: [ adaptations: [
[videoStream], filteredStreams.video,
[audioStream], filteredStreams.audio,
] ]
}; };
} }
@@ -527,6 +575,26 @@ var YouTubeClient = GObject.registerClass({
return reduced; 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) _getPlayerInfoPromise(videoId)
{ {
const data = this._getPlayerPostData(videoId); const data = this._getPlayerPostData(videoId);