mirror of
https://github.com/iv-org/invidious.git
synced 2026-07-28 00:16:47 -05:00
579 lines
19 KiB
JavaScript
579 lines
19 KiB
JavaScript
// Port of FreeTube's SabrManifestParser.js
|
|
// Reads shaka from window.shaka, index parsers from window globals.
|
|
// Registers a manifest parser for the 'application/sabr+json' mime type.
|
|
// Exposes window.MANIFEST_TYPE_SABR.
|
|
|
|
'use strict';
|
|
|
|
(function () {
|
|
var shaka = window.shaka;
|
|
var NetworkingEngine = shaka.net.NetworkingEngine;
|
|
var MANIFEST_TYPE_SABR = 'application/sabr+json';
|
|
var CODECS_REGEX = /codecs="?([^"]+)"?/;
|
|
var VIDEO_CODEC_PRIORITIES = ['av01', 'vp09', 'vp9', 'avc1'];
|
|
|
|
function buildFormatId(format) {
|
|
return format.itag + '-' + (format.lastModified ?? '0') + '-' + (format.xtags ?? '');
|
|
}
|
|
|
|
function createVodMediaSegmentIndex(url, response, format, stream, duration) {
|
|
var mediaQuality = {
|
|
contentType: stream.type,
|
|
bandwidth: stream.bandwidth,
|
|
mimeType: stream.mimeType,
|
|
codecs: stream.codecs,
|
|
language: stream.language,
|
|
label: stream.label,
|
|
audioSamplingRate: stream.audioSamplingRate,
|
|
channelsCount: stream.channelsCount,
|
|
width: stream.width ?? null,
|
|
height: stream.height ?? null,
|
|
frameRate: stream.frameRate ?? null,
|
|
roles: stream.roles,
|
|
pixelAspectRatio: stream.pixelAspectRatio ?? null
|
|
};
|
|
|
|
var buffer = ArrayBuffer.isView(response.data) ? response.data.buffer : response.data;
|
|
var initData = buffer.slice(format.initRange.start, format.initRange.end + 1);
|
|
var indexData = buffer.slice(format.indexRange.start, format.indexRange.end + 1);
|
|
|
|
var initUrls = [url + '&init'];
|
|
var initSegmentReference = new shaka.media.InitSegmentReference(
|
|
function () { return initUrls; },
|
|
format.initRange.start,
|
|
format.initRange.end,
|
|
mediaQuality,
|
|
null,
|
|
initData,
|
|
null
|
|
);
|
|
|
|
initSegmentReference.mimeType = stream.mimeType;
|
|
initSegmentReference.codecs = stream.codecs;
|
|
|
|
var references;
|
|
if (stream.mimeType.endsWith('/webm')) {
|
|
references = window.parseWebmSegmentIndex(indexData, initData, url, initSegmentReference, 0, 0, duration);
|
|
} else {
|
|
references = window.parseMp4SegmentIndex(indexData, format.indexRange.start, url, initSegmentReference, 0, 0, duration);
|
|
}
|
|
|
|
for (var i = 0; i < references.length; i++) {
|
|
references[i].mimeType = stream.mimeType;
|
|
references[i].codecs = stream.codecs;
|
|
}
|
|
|
|
return new shaka.media.SegmentIndex(references);
|
|
}
|
|
|
|
async function createMediaSegmentIndex(format, stream, presentationTimeline, networkingEngine, fakeVideoFormatId) {
|
|
var url = 'sabr:' + stream.type + '?formatId=' + encodeURIComponent(stream.originalId);
|
|
if (fakeVideoFormatId) {
|
|
url += '&videoFormatId=' + encodeURIComponent(fakeVideoFormatId);
|
|
}
|
|
if (format.isDrc) {
|
|
url += '&drc';
|
|
} else if (format.isVoiceBoost) {
|
|
url += '&vb';
|
|
}
|
|
if (stream.type === 'video') {
|
|
var resolution = format.height || 360;
|
|
url += '&resolution=' + resolution;
|
|
}
|
|
|
|
var request = {
|
|
method: 'GET',
|
|
uris: [url + '&init'],
|
|
contentType: stream.type,
|
|
body: null,
|
|
headers: {},
|
|
allowCrossSiteCredentials: false,
|
|
retryParameters: NetworkingEngine.defaultRetryParameters(),
|
|
licenseRequestType: null,
|
|
sessionId: null,
|
|
drmInfo: null,
|
|
initData: null,
|
|
initDataType: null,
|
|
streamDataCallback: null
|
|
};
|
|
|
|
var response = await networkingEngine.request(
|
|
NetworkingEngine.RequestType.SEGMENT,
|
|
request,
|
|
{
|
|
stream: stream,
|
|
type: NetworkingEngine.AdvancedRequestType.INIT_SEGMENT
|
|
}
|
|
).promise;
|
|
|
|
return createVodMediaSegmentIndex(url, response, format, stream, presentationTimeline.getDuration());
|
|
}
|
|
|
|
function createAudioStream(format, id, hasDrcAudio, hasVoiceBoostAudio, presentationTimeline, networkingEngine, fakeVideoFormatId) {
|
|
var roles = [];
|
|
|
|
if (format.isDrc) {
|
|
roles.push('drc');
|
|
} else if (format.isVoiceBoost) {
|
|
roles.push('voice-boost');
|
|
} else if (format.isDubbed) {
|
|
roles.push('dubbed');
|
|
} else if (format.isAutoDubbed) {
|
|
roles.push('dubbed-auto');
|
|
} else if (format.isDescriptive) {
|
|
roles.push('descriptive');
|
|
} else if (format.isSecondary) {
|
|
roles.push('secondary');
|
|
} else if (format.isOriginal) {
|
|
roles.push('main');
|
|
}
|
|
|
|
var label = null;
|
|
if (format.label) {
|
|
if (format.isDrc) {
|
|
label = format.label + ' (Stable Volume)';
|
|
} else if (format.isVoiceBoost) {
|
|
label = format.label + ' (Voice Boost)';
|
|
} else {
|
|
label = format.label;
|
|
}
|
|
} else if (hasDrcAudio || hasVoiceBoostAudio) {
|
|
if (format.isDrc) {
|
|
label = 'Stable Volume';
|
|
} else if (format.isVoiceBoost) {
|
|
label = 'Voice Boost';
|
|
} else {
|
|
label = 'Original';
|
|
}
|
|
}
|
|
|
|
var stream = {
|
|
type: 'audio',
|
|
id: id,
|
|
originalId: buildFormatId(format),
|
|
mimeType: format.mimeType.split(';', 1)[0],
|
|
codecs: format.mimeType.match(CODECS_REGEX)[1],
|
|
fullMimeTypes: new Set([format.mimeType]),
|
|
bandwidth: format.bitrate,
|
|
audioSamplingRate: format.audioSampleRate ?? null,
|
|
channelsCount: format.audioChannels ?? null,
|
|
label: label,
|
|
language: format.language ?? 'und',
|
|
originalLanguage: format.language ?? null,
|
|
spatialAudio: format.spatialAudio,
|
|
roles: roles,
|
|
primary: roles.indexOf('main') !== -1,
|
|
segmentIndex: null,
|
|
createSegmentIndex: async function () {
|
|
if (stream.segmentIndex) return;
|
|
stream.segmentIndex = await createMediaSegmentIndex(format, stream, presentationTimeline, networkingEngine, fakeVideoFormatId);
|
|
},
|
|
closeSegmentIndex: function () {
|
|
if (stream.segmentIndex) {
|
|
stream.segmentIndex.release();
|
|
stream.segmentIndex = null;
|
|
}
|
|
},
|
|
accessibilityPurpose: null,
|
|
closedCaptions: null,
|
|
drmInfos: [],
|
|
emsgSchemeIdUris: null,
|
|
encrypted: false,
|
|
external: false,
|
|
fastSwitching: false,
|
|
forced: false,
|
|
groupId: null,
|
|
isAudioMuxedInVideo: false,
|
|
keyIds: new Set(),
|
|
trickModeVideo: null
|
|
};
|
|
|
|
return stream;
|
|
}
|
|
|
|
function createVideoStream(format, id, presentationTimeline, networkingEngine) {
|
|
var colorGamut = format.colorPrimaries === 'BT2020' ? 'rec2020' : 'srgb';
|
|
var hdr = 'SDR';
|
|
if (format.colorTransferCharacteristics === 'SMPTEST2084') {
|
|
hdr = 'PQ';
|
|
} else if (format.colorTransferCharacteristics === 'ARIB_STD_B67') {
|
|
hdr = 'HLG';
|
|
}
|
|
|
|
var stream = {
|
|
type: 'video',
|
|
id: id,
|
|
originalId: buildFormatId(format),
|
|
mimeType: format.mimeType.split(';', 1)[0],
|
|
codecs: format.mimeType.match(CODECS_REGEX)[1],
|
|
fullMimeTypes: new Set([format.mimeType]),
|
|
bandwidth: format.bitrate,
|
|
width: format.width,
|
|
height: format.height,
|
|
frameRate: format.frameRate,
|
|
colorGamut: colorGamut,
|
|
hdr: hdr,
|
|
roles: [],
|
|
segmentIndex: null,
|
|
createSegmentIndex: async function () {
|
|
if (stream.segmentIndex) return;
|
|
stream.segmentIndex = await createMediaSegmentIndex(format, stream, presentationTimeline, networkingEngine);
|
|
},
|
|
closeSegmentIndex: function () {
|
|
if (stream.segmentIndex) {
|
|
stream.segmentIndex.release();
|
|
stream.segmentIndex = null;
|
|
}
|
|
},
|
|
accessibilityPurpose: null,
|
|
audioSamplingRate: null,
|
|
channelsCount: null,
|
|
closedCaptions: null,
|
|
drmInfos: [],
|
|
emsgSchemeIdUris: null,
|
|
encrypted: false,
|
|
external: false,
|
|
fastSwitching: false,
|
|
forced: false,
|
|
groupId: null,
|
|
isAudioMuxedInVideo: false,
|
|
keyIds: new Set(),
|
|
label: null,
|
|
language: 'und',
|
|
originalLanguage: null,
|
|
primary: false,
|
|
spatialAudio: false,
|
|
trickModeVideo: null
|
|
};
|
|
|
|
return stream;
|
|
}
|
|
|
|
function createTextStreams(captions, presentationTimeline, currentId) {
|
|
var result = [];
|
|
for (var i = 0; i < captions.length; i++) {
|
|
var caption = captions[i];
|
|
var stream = {
|
|
type: 'text',
|
|
id: currentId++,
|
|
originalId: caption.id,
|
|
mimeType: caption.mimeType,
|
|
fullMimeTypes: new Set([caption.mimeType]),
|
|
label: caption.label,
|
|
language: caption.language,
|
|
originalLanguage: caption.language,
|
|
kind: 'captions',
|
|
segmentIndex: null,
|
|
createSegmentIndex: function () {
|
|
stream.segmentIndex = shaka.media.SegmentIndex.forSingleSegment(
|
|
0,
|
|
presentationTimeline.getDuration(),
|
|
[caption.url]
|
|
);
|
|
stream.segmentIndex.get(0).mimeType = caption.mimeType;
|
|
return Promise.resolve();
|
|
},
|
|
closeSegmentIndex: function () {
|
|
if (stream.segmentIndex) {
|
|
stream.segmentIndex.release();
|
|
stream.segmentIndex = null;
|
|
}
|
|
},
|
|
accessibilityPurpose: null,
|
|
audioSamplingRate: null,
|
|
channelsCount: null,
|
|
closedCaptions: null,
|
|
codecs: '',
|
|
drmInfos: [],
|
|
emsgSchemeIdUris: null,
|
|
encrypted: false,
|
|
external: false,
|
|
fastSwitching: false,
|
|
forced: false,
|
|
groupId: null,
|
|
isAudioMuxedInVideo: false,
|
|
keyIds: new Set(),
|
|
primary: false,
|
|
roles: [],
|
|
spatialAudio: false,
|
|
trickModeVideo: null
|
|
};
|
|
result.push(stream);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function createImageStreams(storyboards, presentationTimeline, currentId) {
|
|
var result = [];
|
|
for (var i = 0; i < storyboards.length; i++) {
|
|
var storyboard = storyboards[i];
|
|
var tilesLayout = storyboard.columns + 'x' + storyboard.rows;
|
|
|
|
var stream = {
|
|
type: 'image',
|
|
id: currentId++,
|
|
mimeType: storyboard.mimeType,
|
|
fullMimeTypes: new Set([storyboard.mimeType]),
|
|
tilesLayout: tilesLayout,
|
|
width: storyboard.thumbnailWidth * storyboard.columns,
|
|
height: storyboard.thumbnailHeight * storyboard.rows,
|
|
segmentIndex: null,
|
|
createSegmentIndex: function () {
|
|
var duration = presentationTimeline.getDuration();
|
|
var interval = storyboard.interval > 0 ? storyboard.interval : duration / storyboard.thumbnailCount;
|
|
var segmentDuration = interval * storyboard.columns * storyboard.rows;
|
|
var references = [];
|
|
for (var j = 0; j < storyboard.storyboardCount; j++) {
|
|
var startTime = j * segmentDuration;
|
|
var endTime = Math.min(startTime + segmentDuration, duration);
|
|
var urls = [storyboard.templateUrl.replace('$M', String(j))];
|
|
var segmentReference = new shaka.media.SegmentReference(
|
|
startTime, endTime,
|
|
function () { return urls; },
|
|
0, null, null, 0, 0, Infinity, undefined, tilesLayout, interval
|
|
);
|
|
segmentReference.mimeType = storyboard.mimeType;
|
|
references.push(segmentReference);
|
|
}
|
|
stream.segmentIndex = new shaka.media.SegmentIndex(references);
|
|
return Promise.resolve();
|
|
},
|
|
closeSegmentIndex: function () {
|
|
if (stream.segmentIndex) {
|
|
stream.segmentIndex.release();
|
|
stream.segmentIndex = null;
|
|
}
|
|
},
|
|
accessibilityPurpose: null,
|
|
audioSamplingRate: null,
|
|
channelsCount: null,
|
|
closedCaptions: null,
|
|
codecs: '',
|
|
drmInfos: [],
|
|
emsgSchemeIdUris: null,
|
|
encrypted: false,
|
|
external: false,
|
|
fastSwitching: false,
|
|
forced: false,
|
|
groupId: null,
|
|
isAudioMuxedInVideo: false,
|
|
keyIds: new Set(),
|
|
label: null,
|
|
language: 'und',
|
|
originalId: null,
|
|
originalLanguage: null,
|
|
primary: false,
|
|
roles: [],
|
|
spatialAudio: false,
|
|
trickModeVideo: null
|
|
};
|
|
result.push(stream);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function createChapterStreams(chapters, currentId) {
|
|
if (chapters.length === 0) {
|
|
return [];
|
|
}
|
|
var references = [];
|
|
for (var i = 0; i < chapters.length; i++) {
|
|
var chapter = chapters[i];
|
|
var reference = new shaka.media.SegmentReference(
|
|
chapter.startSeconds, chapter.endSeconds,
|
|
function () { return []; },
|
|
0, null, null, 0, 0, Infinity
|
|
);
|
|
reference.setMetadata({
|
|
title: chapter.title,
|
|
images: chapter.thumbnail
|
|
? [{ url: chapter.thumbnail.url, width: chapter.thumbnail.width, height: chapter.thumbnail.height }]
|
|
: []
|
|
});
|
|
references.push(reference);
|
|
}
|
|
|
|
var stream = {
|
|
id: currentId,
|
|
originalId: null,
|
|
groupId: null,
|
|
createSegmentIndex: function () { return Promise.resolve(); },
|
|
segmentIndex: new shaka.media.SegmentIndex(references),
|
|
mimeType: 'text/plain',
|
|
codecs: '',
|
|
supplementalCodecs: '',
|
|
kind: '',
|
|
encrypted: false,
|
|
drmInfos: [],
|
|
keyIds: new Set(),
|
|
language: 'und',
|
|
originalLanguage: 'und',
|
|
label: null,
|
|
type: 'chapter',
|
|
primary: false,
|
|
trickModeVideo: null,
|
|
dependencyStream: null,
|
|
emsgSchemeIdUris: null,
|
|
roles: [],
|
|
forced: false,
|
|
channelsCount: null,
|
|
audioSamplingRate: null,
|
|
spatialAudio: false,
|
|
closedCaptions: null,
|
|
accessibilityPurpose: null,
|
|
external: true,
|
|
fastSwitching: false,
|
|
fullMimeTypes: new Set(['text/plain']),
|
|
isAudioMuxedInVideo: false,
|
|
baseOriginalId: null
|
|
};
|
|
return [stream];
|
|
}
|
|
|
|
function SabrManifestParser() {
|
|
this._config = null;
|
|
}
|
|
SabrManifestParser.prototype.banLocation = function (_uri) {};
|
|
SabrManifestParser.prototype.configure = function (config, _isPreloadFn) {
|
|
this._config = config;
|
|
};
|
|
SabrManifestParser.prototype.onInitialVariantChosen = function (_variant) {};
|
|
SabrManifestParser.prototype.setMediaElement = function (_mediaElement) {};
|
|
|
|
SabrManifestParser.prototype.start = async function (uri, playerInterface) {
|
|
var filter = playerInterface.filter;
|
|
var networkingEngine = playerInterface.networkingEngine;
|
|
|
|
var uriPrefixLength = 5 + MANIFEST_TYPE_SABR.length + 1;
|
|
var manifestData = JSON.parse(decodeURIComponent(uri.slice(uriPrefixLength)));
|
|
|
|
var presentationTimeline = new shaka.media.PresentationTimeline(0, 0, true);
|
|
presentationTimeline.setStatic(true);
|
|
presentationTimeline.setSegmentAvailabilityDuration(Infinity);
|
|
presentationTimeline.lockStartTime();
|
|
presentationTimeline.setDuration(manifestData.duration);
|
|
|
|
var currentId = 0;
|
|
var audioStreams = [];
|
|
var videoStreams = [];
|
|
|
|
var hasDrcAudio = false;
|
|
var hasVoiceBoostAudio = false;
|
|
for (var i = 0; i < manifestData.formats.length; i++) {
|
|
if (manifestData.formats[i].isDrc) hasDrcAudio = true;
|
|
if (manifestData.formats[i].isVoiceBoost) hasVoiceBoostAudio = true;
|
|
}
|
|
|
|
var fakeVideoFormatId;
|
|
if (this._config.disableVideo) {
|
|
var worstVideoFormat = null;
|
|
for (var i2 = 0; i2 < manifestData.formats.length; i2++) {
|
|
var currentFormat = manifestData.formats[i2];
|
|
if (currentFormat.width === undefined) continue;
|
|
if (worstVideoFormat === null) {
|
|
worstVideoFormat = currentFormat;
|
|
} else if (currentFormat.bitrate < worstVideoFormat.bitrate) {
|
|
worstVideoFormat = currentFormat;
|
|
}
|
|
}
|
|
fakeVideoFormatId = worstVideoFormat ? buildFormatId(worstVideoFormat) : undefined;
|
|
}
|
|
|
|
for (var i3 = 0; i3 < manifestData.formats.length; i3++) {
|
|
var format = manifestData.formats[i3];
|
|
if (format.mimeType.indexOf('audio/') === 0) {
|
|
if (format.xtags === 'CgcKAnZiEgEx') {
|
|
// Workaround: https://github.com/LuanRT/googlevideo/issues/42
|
|
continue;
|
|
}
|
|
audioStreams.push(createAudioStream(format, currentId++, hasDrcAudio, hasVoiceBoostAudio, presentationTimeline, networkingEngine, fakeVideoFormatId));
|
|
} else if (!this._config.disableVideo) {
|
|
videoStreams.push(createVideoStream(format, currentId++, presentationTimeline, networkingEngine));
|
|
}
|
|
}
|
|
|
|
audioStreams.sort(function (a, b) { return b.bandwidth - a.bandwidth; });
|
|
if (!this._config.disableVideo) {
|
|
videoStreams.sort(function (a, b) {
|
|
return VIDEO_CODEC_PRIORITIES.findIndex(function (codec) { return a.codecs.indexOf(codec) === 0; }) -
|
|
VIDEO_CODEC_PRIORITIES.findIndex(function (codec) { return b.codecs.indexOf(codec) === 0; });
|
|
});
|
|
}
|
|
|
|
var variants = [];
|
|
var variantId = 0;
|
|
if (this._config.disableVideo) {
|
|
for (var i4 = 0; i4 < audioStreams.length; i4++) {
|
|
var stream = audioStreams[i4];
|
|
variants.push({
|
|
id: variantId++,
|
|
audio: stream,
|
|
bandwidth: stream.bandwidth,
|
|
language: stream.language,
|
|
allowedByApplication: true,
|
|
allowedByKeySystem: true,
|
|
decodingInfos: [],
|
|
disabledUntilTime: 0,
|
|
primary: stream.primary,
|
|
video: null
|
|
});
|
|
}
|
|
} else {
|
|
for (var i5 = 0; i5 < audioStreams.length; i5++) {
|
|
for (var j = 0; j < videoStreams.length; j++) {
|
|
variants.push({
|
|
id: variantId++,
|
|
audio: audioStreams[i5],
|
|
video: videoStreams[j],
|
|
bandwidth: audioStreams[i5].bandwidth + videoStreams[j].bandwidth,
|
|
language: audioStreams[i5].language,
|
|
allowedByApplication: true,
|
|
allowedByKeySystem: true,
|
|
decodingInfos: [],
|
|
disabledUntilTime: 0,
|
|
primary: audioStreams[i5].primary
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
var textStreams = createTextStreams(manifestData.captions, presentationTimeline, currentId);
|
|
currentId += textStreams.length;
|
|
|
|
var imageStreams = createImageStreams(manifestData.storyboards, presentationTimeline, currentId);
|
|
currentId += imageStreams.length;
|
|
|
|
var chapterStreams = createChapterStreams(manifestData.chapters, currentId);
|
|
|
|
var manifest = {
|
|
type: 'SABR',
|
|
startTime: 0,
|
|
variants: variants,
|
|
textStreams: textStreams,
|
|
imageStreams: imageStreams,
|
|
chapterStreams: chapterStreams,
|
|
presentationTimeline: presentationTimeline,
|
|
gapCount: 0,
|
|
ignoreManifestTimestampsInSegmentsMode: false,
|
|
isLowLatency: false,
|
|
nextUrl: null,
|
|
offlineSessionIds: [],
|
|
periodCount: 1,
|
|
sequenceMode: false,
|
|
serviceDescription: null
|
|
};
|
|
|
|
await filter(manifest);
|
|
return manifest;
|
|
};
|
|
|
|
SabrManifestParser.prototype.stop = function () {
|
|
this._config = null;
|
|
return Promise.resolve();
|
|
};
|
|
|
|
// Register unconditionally so the data: application/sabr+json manifest works.
|
|
shaka.media.ManifestParser.registerParserByMime(MANIFEST_TYPE_SABR, function () { return new SabrManifestParser(); });
|
|
|
|
window.MANIFEST_TYPE_SABR = MANIFEST_TYPE_SABR;
|
|
})(); |