mirror of
https://github.com/iv-org/invidious.git
synced 2026-07-28 00:16:47 -05:00
711 lines
27 KiB
JavaScript
711 lines
27 KiB
JavaScript
// Port of FreeTube's SabrSchemePlugin.js
|
|
// Registers the 'sabr' networking scheme with Shaka.
|
|
// Reads shaka from window.shaka, googlevideo symbols from window.googlevideo.
|
|
// Invidious is browser-sandboxed, so outbound fetches are routed through
|
|
// SABRHelpers.fetchWithProxy (the Crystal /proxy route) instead of hitting
|
|
// googlevideo directly.
|
|
// Fixes the latent SABR_REDIRECT bug (write sabrStreamState.sabrUrl, not currentState.sabrUrl).
|
|
// Exports window.setupSabrScheme.
|
|
|
|
'use strict';
|
|
|
|
(function () {
|
|
// Lazily resolve dependencies at call time so load order is flexible.
|
|
function shaka() { return window.shaka; }
|
|
function gv() { return window.googlevideo; }
|
|
|
|
function deepCopy(obj) {
|
|
return JSON.parse(JSON.stringify(obj));
|
|
}
|
|
|
|
function formatIdFromString(str) {
|
|
var parts = str.split('-');
|
|
return {
|
|
itag: parseInt(parts[0], 10),
|
|
lastModified: parts[1],
|
|
xtags: parts[2]
|
|
};
|
|
}
|
|
|
|
function createBufferedRange(formatId, buffered, segmentIndex) {
|
|
var endSegmentIndex = segmentIndex.find(buffered.end);
|
|
if (endSegmentIndex == null) {
|
|
endSegmentIndex = segmentIndex.getNumReferences() - 1;
|
|
}
|
|
return {
|
|
formatId: formatId,
|
|
startTimeMs: String(Math.round(buffered.start * 1000)),
|
|
durationMs: String(Math.round((buffered.end - buffered.start) * 1000)),
|
|
startSegmentIndex: segmentIndex.find(buffered.start),
|
|
endSegmentIndex: endSegmentIndex
|
|
};
|
|
}
|
|
|
|
function createFullBufferRange(formatId) {
|
|
var MAX_INT32_VALUE = gv().utils.MAX_INT32_VALUE;
|
|
return {
|
|
formatId: formatId,
|
|
durationMs: MAX_INT32_VALUE,
|
|
startTimeMs: '0',
|
|
startSegmentIndex: parseInt(MAX_INT32_VALUE, 10),
|
|
endSegmentIndex: parseInt(MAX_INT32_VALUE, 10),
|
|
timeRange: {
|
|
durationTicks: MAX_INT32_VALUE,
|
|
startTicks: '0',
|
|
timescale: 1000
|
|
}
|
|
};
|
|
}
|
|
|
|
function fillBufferedRanges(player, manifest, audioFormatsActive, streamIsVideo, streamIsAudio, bufferedRanges, activeVariant) {
|
|
var bufferedInfo = player.getBufferedInfo();
|
|
if (bufferedInfo.audio.length === 0 && bufferedInfo.video.length === 0) return;
|
|
|
|
var activeManifestVariant;
|
|
if (audioFormatsActive) {
|
|
activeManifestVariant = manifest.variants.find(function (variant) {
|
|
return variant.audio.originalId === activeVariant.originalAudioId;
|
|
});
|
|
} else {
|
|
activeManifestVariant = manifest.variants.find(function (variant) {
|
|
return variant.audio.originalId === activeVariant.originalAudioId &&
|
|
variant.video.originalId === activeVariant.originalVideoId;
|
|
});
|
|
}
|
|
|
|
var audioFormatId = formatIdFromString(activeVariant.originalAudioId);
|
|
var audioSegmentIndex = activeManifestVariant.audio.segmentIndex;
|
|
|
|
if (streamIsVideo) {
|
|
bufferedRanges.push(createFullBufferRange(audioFormatId));
|
|
} else {
|
|
for (var i = 0; i < bufferedInfo.audio.length; i++) {
|
|
bufferedRanges.push(createBufferedRange(audioFormatId, bufferedInfo.audio[i], audioSegmentIndex));
|
|
}
|
|
}
|
|
|
|
var videoFormatId;
|
|
var videoSegmentIndex;
|
|
|
|
if (streamIsAudio && bufferedInfo.video.length > 0) {
|
|
videoFormatId = formatIdFromString(activeVariant.originalVideoId);
|
|
bufferedRanges.push(createFullBufferRange(videoFormatId));
|
|
} else {
|
|
for (var j = 0; j < bufferedInfo.video.length; j++) {
|
|
var buffered = bufferedInfo.video[j];
|
|
if (!videoFormatId) {
|
|
videoFormatId = formatIdFromString(activeVariant.originalVideoId);
|
|
}
|
|
if (!videoSegmentIndex) {
|
|
videoSegmentIndex = activeManifestVariant.video.segmentIndex;
|
|
}
|
|
bufferedRanges.push(createBufferedRange(videoFormatId, buffered, videoSegmentIndex));
|
|
}
|
|
}
|
|
}
|
|
|
|
function createCacheResponse(ShakaAbortableOperation, uri, request, data) {
|
|
return ShakaAbortableOperation.completed({
|
|
data: data,
|
|
fromCache: true,
|
|
headers: {},
|
|
originalRequest: request,
|
|
originalUri: uri,
|
|
uri: uri
|
|
});
|
|
}
|
|
|
|
function createRecoverableNetworkError(code) {
|
|
var ShakaError = shaka().util.Error;
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return new ShakaError(ShakaError.Severity.RECOVERABLE, ShakaError.Category.NETWORK, code, args);
|
|
}
|
|
|
|
function prepareSabrContexts(sabrStreamState) {
|
|
var sabrContexts = [];
|
|
var unsentSabrContexts = [];
|
|
sabrStreamState.sabrContexts.forEach(function (ctxUpdate) {
|
|
if (sabrStreamState.activeSabrContextTypes.has(ctxUpdate.type)) {
|
|
sabrContexts.push(ctxUpdate);
|
|
} else {
|
|
unsentSabrContexts.push(ctxUpdate.type);
|
|
}
|
|
});
|
|
return { sabrContexts: sabrContexts, unsentSabrContexts: unsentSabrContexts };
|
|
}
|
|
|
|
function decodePart(part, decoder) {
|
|
if (!part.data.chunks.length) return undefined;
|
|
try {
|
|
var concatenateChunks = gv().utils.concatenateChunks;
|
|
var chunk = part.data.chunks.length === 1 ? part.data.chunks[0] : concatenateChunks(part.data.chunks);
|
|
return decoder.decode(chunk);
|
|
} catch (e) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function createTimeoutController(callback, timeoutMs) {
|
|
return {
|
|
_timeout: setTimeout(callback, timeoutMs),
|
|
_resetCount: 0,
|
|
resetTimeoutOnce: function () {
|
|
if (this._resetCount > 0) return;
|
|
this.clearTimeout();
|
|
this._timeout = setTimeout(callback, timeoutMs);
|
|
this._resetCount++;
|
|
},
|
|
clearTimeout: function () {
|
|
clearTimeout(this._timeout);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Wrap the youtube/googlevideo fetch URL through the Invidious /proxy route,
|
|
// forwarding the UMP headers youtube needs. Returns a fetch-compatible RequestInit.
|
|
function proxyFetch(url, requestInit) {
|
|
var headers = new Headers(requestInit.headers || {});
|
|
// Ensure these headers survive the proxy hop.
|
|
if (!headers.has('x-youtube-client-name')) {
|
|
headers.set('x-youtube-client-name', '3');
|
|
}
|
|
return SABRHelpers.fetchWithProxy(url, Object.assign({}, requestInit, { headers: headers }));
|
|
}
|
|
|
|
async function doRequest(operationInputs, currentState) {
|
|
var ShakaError = shaka().util.Error;
|
|
var protos = gv().protos;
|
|
var ump = gv().ump;
|
|
var utils = gv().utils;
|
|
var UmpReader = ump.UmpReader;
|
|
var CompositeBuffer = ump.CompositeBuffer;
|
|
var UMPPartId = protos.UMPPartId;
|
|
var VideoPlaybackAbrRequest = protos.VideoPlaybackAbrRequest;
|
|
var StreamProtectionStatus = protos.StreamProtectionStatus;
|
|
var SabrError = protos.SabrError;
|
|
var SabrRedirect = protos.SabrRedirect;
|
|
var MediaHeader = protos.MediaHeader;
|
|
var SabrContextSendingPolicy = protos.SabrContextSendingPolicy;
|
|
var SabrContextUpdate = protos.SabrContextUpdate;
|
|
var SabrContextWritePolicy = protos.SabrContextWritePolicy;
|
|
var NextRequestPolicy = protos.NextRequestPolicy;
|
|
var PlaybackCookie = protos.PlaybackCookie;
|
|
var ReloadPlaybackContext = protos.ReloadPlaybackContext;
|
|
|
|
var response;
|
|
var chunkedDataBuffer = null;
|
|
var responseDataChunks = [];
|
|
var segmentComplete = false;
|
|
var shouldRetry = false;
|
|
var shouldRetryDueToNextRequestPolicy = false;
|
|
var invalidPoToken = false;
|
|
var error;
|
|
|
|
if (currentState.sabrStreamState.playerReloadRequested) {
|
|
throw createRecoverableNetworkError(ShakaError.Code.OPERATION_ABORTED, operationInputs.uri, operationInputs.requestType);
|
|
}
|
|
|
|
try {
|
|
var shouldReloadDueToBackoffLoop = false;
|
|
if ((currentState.sabrStreamState.nextRequestPolicy?.backoffTimeMs || 0) > 0) {
|
|
var currentBackoffTimeMs = currentState.sabrStreamState.nextRequestPolicy.backoffTimeMs;
|
|
currentState.eventEmitter.emit('backoff-requested', { backoffMs: currentBackoffTimeMs });
|
|
await new Promise(function (resolve, reject) {
|
|
setTimeout(resolve, currentBackoffTimeMs);
|
|
currentState.abortController.signal.addEventListener('abort', reject);
|
|
});
|
|
currentState.timeoutController?.resetTimeoutOnce();
|
|
|
|
currentState.cumulativeBackOffTimeMs += currentState.sabrStreamState.nextRequestPolicy.backoffTimeMs;
|
|
currentState.cumulativeBackOffRequested += 1;
|
|
var timeoutMs = operationInputs.request.retryParameters.timeout;
|
|
if (currentState.cumulativeBackOffRequested >= 3 || (timeoutMs > 0 && timeoutMs <= (currentState.cumulativeBackOffTimeMs + currentBackoffTimeMs))) {
|
|
shouldReloadDueToBackoffLoop = true;
|
|
}
|
|
}
|
|
if (shouldReloadDueToBackoffLoop || currentState.cumulativeRetryDueToNextRequestPolicy >= 100) {
|
|
currentState.sabrStreamState.playerReloadRequested = true;
|
|
if (!currentState.abortController.signal.aborted) {
|
|
currentState.abortController.abort();
|
|
currentState.eventEmitter.emit('reload');
|
|
}
|
|
}
|
|
|
|
var sabrURL = new URL(currentState.sabrStreamState.sabrUrl);
|
|
sabrURL.searchParams.set('rn', String(currentState.sabrStreamState.requestNumber++));
|
|
// Stream through the Invidious proxy (CSP + CORS), honoring requestInit headers/body.
|
|
response = await proxyFetch(sabrURL.toString(), currentState.requestInit);
|
|
|
|
operationInputs.headersReceived({});
|
|
|
|
var fmtId = formatIdFromString(operationInputs.formatIdString);
|
|
var itag = fmtId.itag;
|
|
var lastModified = fmtId.lastModified;
|
|
var xtags = fmtId.xtags;
|
|
var mediaHeaderId;
|
|
|
|
var reader = response.body.getReader();
|
|
var readObj = await reader.read();
|
|
|
|
while (!readObj.done && !currentState.abortStatus.finished) {
|
|
if (chunkedDataBuffer) {
|
|
chunkedDataBuffer.append(readObj.value);
|
|
} else {
|
|
chunkedDataBuffer = new CompositeBuffer([readObj.value]);
|
|
}
|
|
|
|
var remainingData = new UmpReader(chunkedDataBuffer).read(function (part) {
|
|
switch (part.type) {
|
|
case UMPPartId.STREAM_PROTECTION_STATUS: {
|
|
var streamProtectionStatus = decodePart(part, StreamProtectionStatus);
|
|
if (streamProtectionStatus && streamProtectionStatus.status === 3) {
|
|
invalidPoToken = true;
|
|
}
|
|
break;
|
|
}
|
|
case UMPPartId.SABR_ERROR: {
|
|
var sabrError = decodePart(part, SabrError);
|
|
if (!sabrError) break;
|
|
error = 'SABR Error: type: ' + sabrError.type + ', code: ' + sabrError.code;
|
|
break;
|
|
}
|
|
case UMPPartId.SABR_REDIRECT: {
|
|
var sabrRedirect = decodePart(part, SabrRedirect);
|
|
if (!sabrRedirect) break;
|
|
// BUGFIX (vs FreeTube): the read site reads sabrStreamState.sabrUrl,
|
|
// so write there, not currentState.sabrUrl.
|
|
currentState.sabrStreamState.sabrUrl = sabrRedirect.url;
|
|
shouldRetry = true;
|
|
break;
|
|
}
|
|
case UMPPartId.MEDIA_HEADER: {
|
|
if (mediaHeaderId === undefined) {
|
|
var mediaHeader = decodePart(part, MediaHeader);
|
|
if (!mediaHeader) break;
|
|
if (
|
|
mediaHeader.formatId.itag === itag &&
|
|
mediaHeader.formatId.lastModified === lastModified &&
|
|
mediaHeader.formatId.xtags === xtags
|
|
) {
|
|
if (operationInputs.isInit && mediaHeader.isInitSeg) {
|
|
mediaHeaderId = mediaHeader.headerId;
|
|
} else if (!operationInputs.isInit && mediaHeader.sequenceNumber === operationInputs.sequenceNumber) {
|
|
mediaHeaderId = mediaHeader.headerId;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case UMPPartId.MEDIA: {
|
|
if (mediaHeaderId === part.data.getUint8(0)) {
|
|
var split = part.data.split(1);
|
|
var remaining = split.remainingBuffer;
|
|
for (var k = 0; k < remaining.chunks.length; k++) {
|
|
responseDataChunks.push(remaining.chunks[k]);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case UMPPartId.MEDIA_END: {
|
|
if (mediaHeaderId === part.data.getUint8(0)) {
|
|
segmentComplete = true;
|
|
currentState.abortStatus.finished = true;
|
|
currentState.abortController.abort();
|
|
}
|
|
break;
|
|
}
|
|
case UMPPartId.NEXT_REQUEST_POLICY: {
|
|
var nextRequestPolicy = decodePart(part, NextRequestPolicy);
|
|
shouldRetry = true;
|
|
shouldRetryDueToNextRequestPolicy = true;
|
|
currentState.sabrStreamState.nextRequestPolicy = nextRequestPolicy;
|
|
currentState.abrRequest.streamerContext.playbackCookie = nextRequestPolicy?.playbackCookie ? PlaybackCookie.encode(nextRequestPolicy.playbackCookie).finish() : undefined;
|
|
currentState.abrRequest.streamerContext.backoffTimeMs = nextRequestPolicy?.backoffTimeMs;
|
|
break;
|
|
}
|
|
case UMPPartId.FORMAT_INITIALIZATION_METADATA: {
|
|
break;
|
|
}
|
|
case UMPPartId.SABR_CONTEXT_UPDATE: {
|
|
var sabrContextUpdate = decodePart(part, SabrContextUpdate);
|
|
if (!sabrContextUpdate) break;
|
|
if (sabrContextUpdate.type !== undefined && sabrContextUpdate.value?.length) {
|
|
if (
|
|
sabrContextUpdate.writePolicy === SabrContextWritePolicy.KEEP_EXISTING &&
|
|
currentState.sabrStreamState.sabrContexts.has(sabrContextUpdate.type)
|
|
) {
|
|
break;
|
|
}
|
|
currentState.sabrStreamState.sabrContexts.set(sabrContextUpdate.type, sabrContextUpdate);
|
|
if (sabrContextUpdate.sendByDefault) {
|
|
currentState.sabrStreamState.activeSabrContextTypes.add(sabrContextUpdate.type);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case UMPPartId.SABR_CONTEXT_SENDING_POLICY: {
|
|
var sabrContextSendingPolicy = decodePart(part, SabrContextSendingPolicy);
|
|
if (!sabrContextSendingPolicy) break;
|
|
for (var i = 0; i < sabrContextSendingPolicy.startPolicy.length; i++) {
|
|
var startPolicy = sabrContextSendingPolicy.startPolicy[i];
|
|
if (!currentState.sabrStreamState.activeSabrContextTypes.has(startPolicy)) {
|
|
currentState.sabrStreamState.activeSabrContextTypes.add(startPolicy);
|
|
}
|
|
}
|
|
for (var j = 0; j < sabrContextSendingPolicy.stopPolicy.length; j++) {
|
|
var stopPolicy = sabrContextSendingPolicy.stopPolicy[j];
|
|
if (currentState.sabrStreamState.activeSabrContextTypes.has(stopPolicy)) {
|
|
currentState.sabrStreamState.activeSabrContextTypes.delete(stopPolicy);
|
|
}
|
|
}
|
|
for (var m = 0; m < sabrContextSendingPolicy.discardPolicy.length; m++) {
|
|
var discardPolicy = sabrContextSendingPolicy.discardPolicy[m];
|
|
if (currentState.sabrStreamState.sabrContexts.has(discardPolicy)) {
|
|
currentState.sabrStreamState.sabrContexts.delete(discardPolicy);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case UMPPartId.RELOAD_PLAYER_RESPONSE: {
|
|
var reloadPlaybackContext = decodePart(part, ReloadPlaybackContext);
|
|
if (!reloadPlaybackContext) break;
|
|
currentState.sabrStreamState.playerReloadRequested = true;
|
|
if (!currentState.abortController.signal.aborted) {
|
|
currentState.abortController.abort();
|
|
currentState.eventEmitter.emit('reload');
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!currentState.abortStatus.finished) {
|
|
if (remainingData) {
|
|
chunkedDataBuffer = remainingData.data;
|
|
} else {
|
|
chunkedDataBuffer = null;
|
|
}
|
|
readObj = await reader.read();
|
|
}
|
|
}
|
|
} catch (err) {
|
|
if (currentState.abortStatus.cancelled) {
|
|
throw createRecoverableNetworkError(ShakaError.Code.OPERATION_ABORTED, operationInputs.uri, operationInputs.requestType);
|
|
} else if (currentState.abortStatus.timedOut) {
|
|
throw createRecoverableNetworkError(ShakaError.Code.TIMEOUT, operationInputs.uri, operationInputs.requestType);
|
|
} else if (!currentState.abortStatus.finished) {
|
|
throw createRecoverableNetworkError(ShakaError.Code.HTTP_ERROR, operationInputs.uri, err, operationInputs.requestType);
|
|
}
|
|
}
|
|
|
|
if (currentState.abortStatus.cancelled) {
|
|
throw createRecoverableNetworkError(ShakaError.Code.OPERATION_ABORTED, operationInputs.uri, operationInputs.requestType);
|
|
} else if (currentState.abortStatus.timedOut) {
|
|
throw createRecoverableNetworkError(ShakaError.Code.TIMEOUT, operationInputs.uri, operationInputs.requestType);
|
|
}
|
|
|
|
if (responseDataChunks.length > 0 && segmentComplete) {
|
|
var concatenateChunks = utils.concatenateChunks;
|
|
var data = concatenateChunks(responseDataChunks);
|
|
if (operationInputs.isInit) {
|
|
currentState.initDataCache.set(operationInputs.formatIdString, data);
|
|
}
|
|
return {
|
|
uri: operationInputs.uri,
|
|
originalUri: operationInputs.uri,
|
|
data: data,
|
|
status: response.status,
|
|
headers: {},
|
|
fromCache: false,
|
|
originalRequest: operationInputs.request
|
|
};
|
|
} else if (shouldRetry) {
|
|
if (shouldRetryDueToNextRequestPolicy) {
|
|
currentState.cumulativeRetryDueToNextRequestPolicy += 1;
|
|
}
|
|
|
|
var prepared = prepareSabrContexts(currentState.sabrStreamState);
|
|
currentState.abrRequest.streamerContext.sabrContexts = prepared.sabrContexts;
|
|
currentState.abrRequest.streamerContext.unsentSabrContexts = prepared.unsentSabrContexts;
|
|
|
|
var body;
|
|
try {
|
|
body = VideoPlaybackAbrRequest.encode(currentState.abrRequest).finish();
|
|
} catch (e) {
|
|
console.error('Invalid VideoPlaybackAbrRequest data', currentState.abrRequest);
|
|
throw e;
|
|
}
|
|
|
|
currentState.requestInit = {
|
|
body: body,
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/x-protobuf',
|
|
'accept-encoding': 'identity',
|
|
'accept': 'application/vnd.yt-ump'
|
|
},
|
|
signal: currentState.abortController.signal
|
|
};
|
|
currentState.abortStatus.timedOut = false;
|
|
currentState.abortStatus.finished = false;
|
|
return doRequest(operationInputs, currentState);
|
|
} else if (invalidPoToken) {
|
|
throw new ShakaError(
|
|
ShakaError.Severity.CRITICAL,
|
|
ShakaError.Category.NETWORK,
|
|
ShakaError.Code.HTTP_ERROR,
|
|
operationInputs.uri,
|
|
new Error('Invalid PO token'),
|
|
operationInputs.requestType
|
|
);
|
|
} else if (error) {
|
|
throw createRecoverableNetworkError(ShakaError.Code.HTTP_ERROR, operationInputs.uri, new Error(error), operationInputs.requestType);
|
|
} else if (responseDataChunks.length > 0 && !segmentComplete) {
|
|
throw createRecoverableNetworkError(
|
|
ShakaError.Code.HTTP_ERROR,
|
|
operationInputs.uri,
|
|
new Error('Incomplete segment, missing MEDIA_END part'),
|
|
operationInputs.requestType
|
|
);
|
|
} else if (response.status === 200) {
|
|
throw createRecoverableNetworkError(
|
|
ShakaError.Code.HTTP_ERROR,
|
|
operationInputs.uri,
|
|
new Error('Empty response, this should not happen'),
|
|
operationInputs.requestType
|
|
);
|
|
} else {
|
|
var severity = response.status === 401 || response.status === 403
|
|
? ShakaError.Severity.CRITICAL
|
|
: ShakaError.Severity.RECOVERABLE;
|
|
throw new ShakaError(
|
|
severity,
|
|
ShakaError.Category.NETWORK,
|
|
ShakaError.Code.BAD_HTTP_STATUS,
|
|
operationInputs.uri,
|
|
response.status,
|
|
'',
|
|
{},
|
|
operationInputs.requestType,
|
|
operationInputs.uri
|
|
);
|
|
}
|
|
}
|
|
|
|
function setupSabrScheme(sabrData, getPlayer, getManifest, getWidth, getHeight) {
|
|
var ShakaAbortableOperation = shaka().util.AbortableOperation;
|
|
var ShakaError = shaka().util.Error;
|
|
var protos = gv().protos;
|
|
var utils = gv().utils;
|
|
var base64ToU8 = utils.base64ToU8;
|
|
var VideoPlaybackAbrRequest = protos.VideoPlaybackAbrRequest;
|
|
var PlaybackCookie = protos.PlaybackCookie;
|
|
var EventEmitterLike = utils.EventEmitterLike;
|
|
|
|
var eventEmitter = new EventEmitterLike();
|
|
var initDataCache = new Map();
|
|
|
|
var poToken = base64ToU8(sabrData.poToken);
|
|
var videoPlaybackUstreamerConfig = base64ToU8(sabrData.ustreamerConfig);
|
|
var clientInfo = deepCopy(sabrData.clientInfo);
|
|
|
|
var sabrStreamState = {
|
|
sabrUrl: sabrData.url,
|
|
activeSabrContextTypes: new Set(),
|
|
sabrContexts: new Map(),
|
|
nextRequestPolicy: undefined,
|
|
playerReloadRequested: false,
|
|
requestNumber: 0
|
|
};
|
|
|
|
shaka().net.NetworkingEngine.registerScheme('sabr', function (uri, request, requestType, _progressUpdated, headersReceived, _config) {
|
|
var player = getPlayer();
|
|
if (player == null) {
|
|
return new ShakaAbortableOperation(Promise.resolve());
|
|
}
|
|
var isAudioOnly = player.isAudioOnly();
|
|
|
|
var url = new URL(request.uris[0]);
|
|
var isInit = url.searchParams.has('init');
|
|
var formatIdString = url.searchParams.get('formatId');
|
|
|
|
if (isInit && initDataCache.has(formatIdString)) {
|
|
return createCacheResponse(ShakaAbortableOperation, uri, request, initDataCache.get(formatIdString));
|
|
}
|
|
|
|
var variantTracks = player.getVariantTracks();
|
|
var activeVariant = null;
|
|
for (var i = 0; i < variantTracks.length; i++) {
|
|
if (variantTracks[i].active) { activeVariant = variantTracks[i]; break; }
|
|
}
|
|
|
|
var streamIsAudio = url.pathname === 'audio';
|
|
var streamIsVideo = url.pathname === 'video';
|
|
|
|
var audioFormatId;
|
|
var videoFormatId;
|
|
|
|
if (streamIsAudio) {
|
|
audioFormatId = formatIdFromString(formatIdString);
|
|
if (isAudioOnly) {
|
|
videoFormatId = formatIdFromString(url.searchParams.get('videoFormatId'));
|
|
} else {
|
|
videoFormatId = formatIdFromString((activeVariant || variantTracks[0]).originalVideoId);
|
|
}
|
|
} else if (streamIsVideo) {
|
|
videoFormatId = formatIdFromString(formatIdString);
|
|
if (activeVariant) {
|
|
audioFormatId = formatIdFromString(activeVariant.originalAudioId);
|
|
} else {
|
|
var candidates = variantTracks.filter(function (track) {
|
|
return track.audioRoles.indexOf('main') !== -1;
|
|
});
|
|
var probableAudioFormat = candidates.reduce(function (previous, current) {
|
|
return current.audioBandwidth >= previous.audioBandwidth ? current : previous;
|
|
}, candidates[0]);
|
|
audioFormatId = formatIdFromString(probableAudioFormat.originalAudioId);
|
|
}
|
|
}
|
|
|
|
var bufferedRanges = [];
|
|
if (!isInit && activeVariant) {
|
|
fillBufferedRanges(player, getManifest(), isAudioOnly, streamIsVideo, streamIsAudio, bufferedRanges, activeVariant);
|
|
}
|
|
|
|
var playerTimeMs = '0';
|
|
if (url.searchParams.has('startTimeMs')) {
|
|
playerTimeMs = url.searchParams.get('startTimeMs');
|
|
}
|
|
|
|
var drcEnabled = url.searchParams.has('drc') || !!(activeVariant && activeVariant.audioRoles.indexOf('drc') !== -1);
|
|
var enableVoiceBoost = url.searchParams.has('vb') || !!(activeVariant && activeVariant.audioRoles.indexOf('vb') !== -1);
|
|
var resolution = streamIsVideo ? parseInt(url.searchParams.get('resolution'), 10) : undefined;
|
|
|
|
var prepared = prepareSabrContexts(sabrStreamState);
|
|
|
|
var requestData = {
|
|
clientAbrState: {
|
|
bandwidthEstimate: String(Math.round(player.getStats().estimatedBandwidth)),
|
|
timeSinceLastManualFormatSelectionMs: streamIsVideo ? '0' : undefined,
|
|
stickyResolution: resolution,
|
|
lastManualSelectedResolution: resolution,
|
|
playbackRate: player.getPlaybackRate(),
|
|
enabledTrackTypesBitfield: streamIsAudio ? 1 : 0,
|
|
drcEnabled: drcEnabled,
|
|
enableVoiceBoost: enableVoiceBoost,
|
|
playerTimeMs: playerTimeMs,
|
|
clientViewportWidth: getWidth(),
|
|
clientViewportHeight: getHeight(),
|
|
clientViewportIsFlexible: false
|
|
},
|
|
preferredAudioFormatIds: [audioFormatId],
|
|
preferredVideoFormatIds: [videoFormatId],
|
|
preferredSubtitleFormatIds: [],
|
|
selectedFormatIds: isInit ? [] : [audioFormatId, videoFormatId],
|
|
bufferedRanges: bufferedRanges,
|
|
streamerContext: {
|
|
poToken: poToken,
|
|
clientInfo: clientInfo,
|
|
sabrContexts: prepared.sabrContexts,
|
|
unsentSabrContexts: prepared.unsentSabrContexts,
|
|
playbackCookie: sabrStreamState.nextRequestPolicy?.playbackCookie ? PlaybackCookie.encode(sabrStreamState.nextRequestPolicy.playbackCookie).finish() : undefined
|
|
},
|
|
field1000: [],
|
|
videoPlaybackUstreamerConfig: videoPlaybackUstreamerConfig
|
|
};
|
|
|
|
var body;
|
|
try {
|
|
body = VideoPlaybackAbrRequest.encode(requestData).finish();
|
|
} catch (e) {
|
|
console.error('Invalid VideoPlaybackAbrRequest data', requestData);
|
|
throw e;
|
|
}
|
|
|
|
var sequenceNumber = parseInt(url.searchParams.get('sq'), 10);
|
|
|
|
var opInputs = {
|
|
uri: uri,
|
|
request: request,
|
|
requestType: requestType,
|
|
headersReceived: headersReceived,
|
|
formatIdString: formatIdString,
|
|
isInit: isInit,
|
|
sequenceNumber: isNaN(sequenceNumber) ? undefined : sequenceNumber
|
|
};
|
|
|
|
var abortController = new AbortController();
|
|
|
|
var init = {
|
|
body: body,
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/x-protobuf',
|
|
'accept-encoding': 'identity',
|
|
'accept': 'application/vnd.yt-ump'
|
|
},
|
|
signal: abortController.signal
|
|
};
|
|
|
|
var abortStatus = { cancelled: false, timedOut: false, finished: false };
|
|
var timeoutMs = request.retryParameters.timeout;
|
|
var timeoutController = null;
|
|
if (timeoutMs) {
|
|
timeoutController = createTimeoutController(function () {
|
|
abortStatus.timedOut = true;
|
|
abortController.abort();
|
|
}, timeoutMs);
|
|
}
|
|
|
|
var currentState = {
|
|
initDataCache: initDataCache,
|
|
abrRequest: requestData,
|
|
requestInit: init,
|
|
abortStatus: abortStatus,
|
|
abortController: abortController,
|
|
sabrStreamState: sabrStreamState,
|
|
timeoutController: timeoutController,
|
|
eventEmitter: eventEmitter,
|
|
cumulativeBackOffTimeMs: 0,
|
|
cumulativeBackOffRequested: 0,
|
|
cumulativeRetryDueToNextRequestPolicy: 0
|
|
};
|
|
|
|
var pendingRequest = doRequest(opInputs, currentState);
|
|
|
|
var op = new ShakaAbortableOperation(pendingRequest, function () {
|
|
abortStatus.cancelled = true;
|
|
abortController.abort();
|
|
return Promise.resolve();
|
|
});
|
|
|
|
if (timeoutController) {
|
|
op.finally(function () {
|
|
timeoutController.clearTimeout();
|
|
});
|
|
}
|
|
|
|
return op;
|
|
});
|
|
|
|
function cleanup() {
|
|
shaka().net.NetworkingEngine.unregisterScheme('sabr');
|
|
initDataCache.clear();
|
|
}
|
|
|
|
return {
|
|
onBackoffRequested: function (callback) {
|
|
eventEmitter.on('backoff-requested', callback);
|
|
},
|
|
onReloadOnce: function (callback) {
|
|
eventEmitter.once('reload', callback);
|
|
},
|
|
cleanup: cleanup
|
|
};
|
|
}
|
|
|
|
window.setupSabrScheme = setupSabrScheme;
|
|
})(); |