mirror of
https://github.com/iv-org/invidious.git
synced 2026-01-28 07:48:31 -06:00
737 lines
28 KiB
JavaScript
737 lines
28 KiB
JavaScript
/**
|
|
* SABR Shaka Player Adapter
|
|
* Ported from Kira project (https://github.com/LuanRT/kira)
|
|
*
|
|
* This module provides the ShakaPlayerAdapter class that implements
|
|
* the SabrPlayerAdapter interface for use with the SABR streaming adapter.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var ShakaPlayerAdapter = (function() {
|
|
/**
|
|
* Convert object to Map
|
|
* @param {Object} object
|
|
* @returns {Map}
|
|
*/
|
|
function asMap(object) {
|
|
var map = new Map();
|
|
for (var key in object) {
|
|
if (Object.prototype.hasOwnProperty.call(object, key)) {
|
|
map.set(key, object[key]);
|
|
}
|
|
}
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* Convert Headers to plain object
|
|
* @param {Headers} headers
|
|
* @returns {Object}
|
|
*/
|
|
function headersToGenericObject(headers) {
|
|
var headersObj = {};
|
|
headers.forEach(function(value, key) {
|
|
headersObj[key.trim()] = value;
|
|
});
|
|
return headersObj;
|
|
}
|
|
|
|
/**
|
|
* Create a Shaka response object
|
|
* @param {Object} headers
|
|
* @param {BufferSource} data
|
|
* @param {number} status
|
|
* @param {string} uri
|
|
* @param {string} responseURL
|
|
* @param {Object} request
|
|
* @param {number} requestType
|
|
* @returns {Object}
|
|
*/
|
|
function makeResponse(headers, data, status, uri, responseURL, request, requestType) {
|
|
if (status >= 200 && status <= 299 && status !== 202) {
|
|
return {
|
|
uri: responseURL || uri,
|
|
originalUri: uri,
|
|
data: data,
|
|
status: status,
|
|
headers: headers,
|
|
originalRequest: request,
|
|
fromCache: !!headers['x-shaka-from-cache']
|
|
};
|
|
}
|
|
|
|
var responseText = null;
|
|
try {
|
|
responseText = shaka.util.StringUtils.fromBytesAutoDetect(data);
|
|
} catch (e) { /* no-op */ }
|
|
|
|
var severity = (status === 401 || status === 403)
|
|
? shaka.util.Error.Severity.CRITICAL
|
|
: shaka.util.Error.Severity.RECOVERABLE;
|
|
|
|
throw new shaka.util.Error(
|
|
severity,
|
|
shaka.util.Error.Category.NETWORK,
|
|
shaka.util.Error.Code.BAD_HTTP_STATUS,
|
|
uri,
|
|
status,
|
|
responseText,
|
|
headers,
|
|
requestType,
|
|
responseURL || uri
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create a recoverable Shaka error
|
|
* @param {string} message
|
|
* @param {Object} info
|
|
* @returns {shaka.util.Error}
|
|
*/
|
|
function createRecoverableError(message, info) {
|
|
return new shaka.util.Error(
|
|
shaka.util.Error.Severity.RECOVERABLE,
|
|
shaka.util.Error.Category.NETWORK,
|
|
shaka.util.Error.Code.HTTP_ERROR,
|
|
message,
|
|
{ info: info }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if URL is a Google Video URL
|
|
* @param {string} url
|
|
* @returns {boolean}
|
|
*/
|
|
function isGoogleVideoURL(url) {
|
|
try {
|
|
var urlObj = new URL(url);
|
|
return urlObj.hostname.endsWith('.googlevideo.com') ||
|
|
urlObj.hostname.endsWith('.youtube.com') ||
|
|
urlObj.hostname.includes('googlevideo');
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ShakaPlayerAdapter class implementing SabrPlayerAdapter interface
|
|
*/
|
|
function ShakaPlayerAdapter() {
|
|
this.player = null;
|
|
this.requestMetadataManager = null;
|
|
this.cacheManager = null;
|
|
this.abortController = null;
|
|
this.requestFilter = null;
|
|
this.responseFilter = null;
|
|
}
|
|
|
|
/**
|
|
* Initialize the adapter with a Shaka player instance
|
|
* @param {shaka.Player} player
|
|
* @param {RequestMetadataManager} requestMetadataManager
|
|
* @param {CacheManager} cacheManager
|
|
*/
|
|
ShakaPlayerAdapter.prototype.initialize = function(player, requestMetadataManager, cacheManager) {
|
|
console.log('[ShakaPlayerAdapter] initialize() called', { player: !!player, requestMetadataManager: !!requestMetadataManager, cacheManager: !!cacheManager });
|
|
var self = this;
|
|
this.player = player;
|
|
this.requestMetadataManager = requestMetadataManager;
|
|
this.cacheManager = cacheManager;
|
|
|
|
var networkingEngine = shaka.net.NetworkingEngine;
|
|
var schemes = ['http', 'https'];
|
|
console.log('[ShakaPlayerAdapter] Registering schemes:', schemes);
|
|
|
|
if (!shaka.net.HttpFetchPlugin.isSupported()) {
|
|
throw new Error('The Fetch API is not supported in this browser.');
|
|
}
|
|
|
|
schemes.forEach(function(scheme) {
|
|
console.log('[ShakaPlayerAdapter] Registering scheme:', scheme);
|
|
networkingEngine.registerScheme(
|
|
scheme,
|
|
self.parseRequest.bind(self),
|
|
networkingEngine.PluginPriority.PREFERRED
|
|
);
|
|
});
|
|
console.log('[ShakaPlayerAdapter] Initialization complete');
|
|
};
|
|
|
|
/**
|
|
* Parse and handle a network request
|
|
*/
|
|
ShakaPlayerAdapter.prototype.parseRequest = function(
|
|
uri, request, requestType, progressUpdated, headersReceived, config
|
|
) {
|
|
var self = this;
|
|
var headers = new Headers();
|
|
asMap(request.headers).forEach(function(value, key) {
|
|
headers.append(key, value);
|
|
});
|
|
|
|
var controller = new AbortController();
|
|
this.abortController = controller;
|
|
|
|
var init = {
|
|
body: request.body || undefined,
|
|
headers: headers,
|
|
method: request.method,
|
|
signal: this.abortController.signal,
|
|
credentials: request.allowCrossSiteCredentials ? 'include' : undefined
|
|
};
|
|
|
|
var abortStatus = { canceled: false, timedOut: false };
|
|
var minBytes = config.minBytesForProgressEvents || 0;
|
|
|
|
var pendingRequest = this.doRequest(
|
|
uri, request, requestType, init, controller,
|
|
abortStatus, progressUpdated, headersReceived, minBytes
|
|
);
|
|
|
|
var operation = new shaka.util.AbortableOperation(
|
|
pendingRequest,
|
|
function() {
|
|
abortStatus.canceled = true;
|
|
controller.abort();
|
|
return Promise.resolve();
|
|
}
|
|
);
|
|
|
|
var timeoutMs = request.retryParameters.timeout;
|
|
if (timeoutMs) {
|
|
var timer = new shaka.util.Timer(function() {
|
|
abortStatus.timedOut = true;
|
|
controller.abort();
|
|
console.warn('[ShakaPlayerAdapter]', 'Request aborted due to timeout:', uri, requestType);
|
|
});
|
|
timer.tickAfter(timeoutMs / 1000);
|
|
operation.finally(function() { timer.stop(); });
|
|
}
|
|
|
|
return operation;
|
|
};
|
|
|
|
/**
|
|
* Handle cached request
|
|
*/
|
|
ShakaPlayerAdapter.prototype.handleCachedRequest = async function(
|
|
requestMetadata, uri, request, progressUpdated, headersReceived, requestType
|
|
) {
|
|
if (!requestMetadata.byteRange || !this.cacheManager) {
|
|
return null;
|
|
}
|
|
|
|
// Check if FormatKeyUtils is available
|
|
if (typeof FormatKeyUtils === 'undefined' || !FormatKeyUtils.createSegmentCacheKeyFromMetadata) {
|
|
return null;
|
|
}
|
|
|
|
var segmentKey = FormatKeyUtils.createSegmentCacheKeyFromMetadata(requestMetadata);
|
|
|
|
var arrayBuffer = requestMetadata.isInit
|
|
? this.cacheManager.getInitSegment(segmentKey)?.buffer
|
|
: this.cacheManager.getSegment(segmentKey)?.buffer;
|
|
|
|
if (!arrayBuffer) {
|
|
return null;
|
|
}
|
|
|
|
if (requestMetadata.isInit) {
|
|
arrayBuffer = arrayBuffer.slice(
|
|
requestMetadata.byteRange.start,
|
|
requestMetadata.byteRange.end + 1
|
|
);
|
|
}
|
|
|
|
var headers = {
|
|
'content-type': requestMetadata.format?.mimeType?.split(';')[0] || '',
|
|
'content-length': arrayBuffer.byteLength.toString(),
|
|
'x-shaka-from-cache': 'true'
|
|
};
|
|
|
|
headersReceived(headers);
|
|
progressUpdated(0, arrayBuffer.byteLength, 0);
|
|
|
|
return makeResponse(headers, arrayBuffer, 200, uri, uri, request, requestType);
|
|
};
|
|
|
|
/**
|
|
* Handle UMP response (SABR streaming format)
|
|
*/
|
|
ShakaPlayerAdapter.prototype.handleUmpResponse = async function(
|
|
response, requestMetadata, uri, request, requestType,
|
|
progressUpdated, abortController, minBytes
|
|
) {
|
|
var self = this;
|
|
var lastTime = Date.now();
|
|
|
|
// Check if SabrUmpProcessor is available
|
|
if (typeof SabrUmpProcessor === 'undefined') {
|
|
console.warn('[ShakaPlayerAdapter]', 'SabrUmpProcessor not available, falling back to normal handling');
|
|
var arrayBuffer = await response.arrayBuffer();
|
|
return this.createShakaResponse({ uri: uri, request: request, requestType: requestType, response: response, arrayBuffer: arrayBuffer });
|
|
}
|
|
|
|
var sabrUmpReader = new SabrUmpProcessor(requestMetadata, this.cacheManager);
|
|
|
|
function checkResultIntegrity(result) {
|
|
if (!result.data && ((!!requestMetadata.error || requestMetadata.streamInfo?.streamProtectionStatus?.status === 3) && !requestMetadata.streamInfo?.sabrContextUpdate)) {
|
|
throw createRecoverableError('Server streaming error', requestMetadata);
|
|
}
|
|
}
|
|
|
|
function shouldReturnEmptyResponse() {
|
|
return requestMetadata.isSABR && (requestMetadata.streamInfo?.redirect || requestMetadata.streamInfo?.sabrContextUpdate);
|
|
}
|
|
|
|
// If response body is not a ReadableStream, handle whole response
|
|
if (!response.body) {
|
|
var arrayBuffer = await response.arrayBuffer();
|
|
var currentTime = Date.now();
|
|
|
|
progressUpdated(currentTime - lastTime, arrayBuffer.byteLength, 0);
|
|
|
|
var result = await sabrUmpReader.processChunk(new Uint8Array(arrayBuffer));
|
|
|
|
if (result) {
|
|
checkResultIntegrity(result);
|
|
return this.createShakaResponse({ uri: uri, request: request, requestType: requestType, response: response, arrayBuffer: result.data });
|
|
}
|
|
|
|
if (shouldReturnEmptyResponse()) {
|
|
return this.createShakaResponse({ uri: uri, request: request, requestType: requestType, response: response, arrayBuffer: undefined });
|
|
}
|
|
|
|
throw createRecoverableError('Empty response with no redirect information', requestMetadata);
|
|
}
|
|
|
|
// Stream processing with ReadableStream
|
|
var reader = response.body.getReader();
|
|
var loaded = 0;
|
|
var lastLoaded = 0;
|
|
var contentLength;
|
|
|
|
while (!abortController.signal.aborted) {
|
|
var readObj;
|
|
try {
|
|
readObj = await reader.read();
|
|
} catch (e) {
|
|
break;
|
|
}
|
|
|
|
var value = readObj.value;
|
|
var done = readObj.done;
|
|
|
|
if (done) {
|
|
if (shouldReturnEmptyResponse()) {
|
|
return this.createShakaResponse({ uri: uri, request: request, requestType: requestType, response: response, arrayBuffer: undefined });
|
|
}
|
|
throw createRecoverableError('Empty response with no redirect information', requestMetadata);
|
|
}
|
|
|
|
var result = await sabrUmpReader.processChunk(value);
|
|
var segmentInfo = sabrUmpReader.getSegmentInfo();
|
|
|
|
if (segmentInfo) {
|
|
if (!contentLength) {
|
|
contentLength = segmentInfo.mediaHeader.contentLength;
|
|
}
|
|
loaded += segmentInfo.lastChunkSize || 0;
|
|
segmentInfo.lastChunkSize = 0;
|
|
}
|
|
|
|
var currentTime = Date.now();
|
|
var chunkSize = loaded - lastLoaded;
|
|
|
|
if ((currentTime - lastTime > 100 && chunkSize >= minBytes) || result) {
|
|
if (result) checkResultIntegrity(result);
|
|
if (contentLength) {
|
|
var numBytesRemaining = result ? 0 : parseInt(contentLength) - loaded;
|
|
try {
|
|
progressUpdated(currentTime - lastTime, chunkSize, numBytesRemaining);
|
|
} catch (e) { /* no-op */ }
|
|
finally {
|
|
lastLoaded = loaded;
|
|
lastTime = currentTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (result) {
|
|
abortController.abort();
|
|
return this.createShakaResponse({ uri: uri, request: request, requestType: requestType, response: response, arrayBuffer: result.data });
|
|
}
|
|
}
|
|
|
|
throw createRecoverableError('UMP stream processing was aborted but did not produce a result.', requestMetadata);
|
|
};
|
|
|
|
/**
|
|
* Perform the network request
|
|
*/
|
|
ShakaPlayerAdapter.prototype.doRequest = async function(
|
|
uri, request, requestType, init, abortController,
|
|
abortStatus, progressUpdated, headersReceived, minBytes
|
|
) {
|
|
var self = this;
|
|
|
|
try {
|
|
console.log('[ShakaPlayerAdapter] doRequest called:', { uri: uri, requestType: requestType });
|
|
|
|
// Convert sabr:// URLs to HTTP URLs before processing
|
|
if (uri.startsWith('sabr://')) {
|
|
// sabr:// URLs should have been converted by the request interceptor
|
|
// If we reach here, the interceptor wasn't set up properly
|
|
console.error('[ShakaPlayerAdapter] *** sabr:// URL reached doRequest without being converted:', uri);
|
|
console.error('[ShakaPlayerAdapter] This means the request interceptor is not working!');
|
|
// Try to handle it anyway - this shouldn't normally happen
|
|
return makeResponse({}, new ArrayBuffer(0), 200, uri, uri, request, requestType);
|
|
}
|
|
|
|
var requestMetadata = this.requestMetadataManager?.getRequestMetadata(uri);
|
|
|
|
// Check cache first
|
|
if (requestMetadata) {
|
|
var cachedResponse = await this.handleCachedRequest(
|
|
requestMetadata, uri, request, progressUpdated, headersReceived, requestType
|
|
);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
}
|
|
|
|
// Proxy Google Video URLs
|
|
var fetchUrl = uri;
|
|
if (isGoogleVideoURL(uri)) {
|
|
// Ensure required headers are present for googlevideo requests
|
|
var headersForProxy = init.headers;
|
|
|
|
// Debug: log initial headers
|
|
console.log('[ShakaPlayerAdapter] Initial init.headers:', init.headers ? 'exists' : 'null',
|
|
'method:', init.method);
|
|
if (init.headers) {
|
|
var initHeadersDebug = [];
|
|
init.headers.forEach(function(v, k) { initHeadersDebug.push(k); });
|
|
console.log('[ShakaPlayerAdapter] Init headers keys:', initHeadersDebug);
|
|
}
|
|
|
|
// For SABR requests (POST to videoplayback), ensure we have proper headers
|
|
if (!headersForProxy || headersForProxy.entries().next().done) {
|
|
headersForProxy = new Headers();
|
|
}
|
|
|
|
// Always add User-Agent for googlevideo requests to avoid 403
|
|
// Use the browser's actual User-Agent
|
|
if (!headersForProxy.has('user-agent')) {
|
|
headersForProxy.set('user-agent', navigator.userAgent);
|
|
}
|
|
|
|
// For POST requests (SABR), add additional required headers
|
|
if (init.method === 'POST') {
|
|
console.log('[ShakaPlayerAdapter] POST request detected, adding content-type header');
|
|
headersForProxy.set('content-type', 'application/x-protobuf');
|
|
if (!headersForProxy.has('origin')) {
|
|
headersForProxy.set('origin', 'https://www.youtube.com');
|
|
}
|
|
if (!headersForProxy.has('referer')) {
|
|
headersForProxy.set('referer', 'https://www.youtube.com/');
|
|
}
|
|
}
|
|
|
|
// Debug: log the headers being sent
|
|
var debugHeaders = [];
|
|
headersForProxy.forEach(function(v, k) { debugHeaders.push([k, v.substring(0, 50)]); });
|
|
console.log('[ShakaPlayerAdapter] Headers for proxy:', debugHeaders);
|
|
|
|
fetchUrl = SABRHelpers.proxyUrl(uri, headersForProxy).toString();
|
|
// Set Content-Type on the fetch request for POST (needed for the proxy to know the body type)
|
|
// Other headers are passed via __headers param and will be forwarded by the proxy
|
|
init.headers = new Headers();
|
|
if (init.method === 'POST') {
|
|
init.headers.set('Content-Type', 'application/x-protobuf');
|
|
}
|
|
}
|
|
|
|
var response = await fetch(fetchUrl, init);
|
|
|
|
// Debug log for POST requests
|
|
if (init.method === 'POST' && init.body) {
|
|
var bodySize = init.body instanceof ArrayBuffer ? init.body.byteLength :
|
|
(init.body instanceof Uint8Array ? init.body.byteLength : 0);
|
|
console.log('[ShakaPlayerAdapter] POST request sent:', {
|
|
url: fetchUrl.substring(0, 100) + '...',
|
|
bodySize: bodySize + ' bytes',
|
|
status: response.status
|
|
});
|
|
}
|
|
|
|
headersReceived(headersToGenericObject(response.headers));
|
|
|
|
// Handle UMP response
|
|
if (requestMetadata && init.method !== 'HEAD' && response.headers.get('content-type') === 'application/vnd.yt-ump') {
|
|
return this.handleUmpResponse(
|
|
response, requestMetadata, uri, request, requestType,
|
|
progressUpdated, abortController, minBytes
|
|
);
|
|
}
|
|
|
|
// Handle normal response
|
|
var lastTime = Date.now();
|
|
var arrayBuffer = await response.arrayBuffer();
|
|
var currentTime = Date.now();
|
|
|
|
progressUpdated(currentTime - lastTime, arrayBuffer.byteLength, 0);
|
|
|
|
return this.createShakaResponse({
|
|
uri: uri,
|
|
request: request,
|
|
requestType: requestType,
|
|
response: response,
|
|
arrayBuffer: arrayBuffer
|
|
});
|
|
} catch (error) {
|
|
if (abortStatus.canceled) {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.RECOVERABLE,
|
|
shaka.util.Error.Category.NETWORK,
|
|
shaka.util.Error.Code.OPERATION_ABORTED,
|
|
uri, requestType
|
|
);
|
|
} else if (abortStatus.timedOut) {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.RECOVERABLE,
|
|
shaka.util.Error.Category.NETWORK,
|
|
shaka.util.Error.Code.TIMEOUT,
|
|
uri, requestType
|
|
);
|
|
}
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.RECOVERABLE,
|
|
shaka.util.Error.Category.NETWORK,
|
|
shaka.util.Error.Code.HTTP_ERROR,
|
|
uri, error, requestType
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check that player is initialized
|
|
*/
|
|
ShakaPlayerAdapter.prototype.checkPlayerStatus = function() {
|
|
if (!this.player) {
|
|
throw new Error('Player not initialized');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get current playback time
|
|
*/
|
|
ShakaPlayerAdapter.prototype.getPlayerTime = function() {
|
|
this.checkPlayerStatus();
|
|
var mediaElement = this.player.getMediaElement();
|
|
return mediaElement ? mediaElement.currentTime : 0;
|
|
};
|
|
|
|
/**
|
|
* Get current playback rate
|
|
*/
|
|
ShakaPlayerAdapter.prototype.getPlaybackRate = function() {
|
|
this.checkPlayerStatus();
|
|
return this.player.getPlaybackRate();
|
|
};
|
|
|
|
/**
|
|
* Get bandwidth estimate
|
|
*/
|
|
ShakaPlayerAdapter.prototype.getBandwidthEstimate = function() {
|
|
this.checkPlayerStatus();
|
|
return this.player.getStats().estimatedBandwidth;
|
|
};
|
|
|
|
/**
|
|
* Get active track formats
|
|
*/
|
|
ShakaPlayerAdapter.prototype.getActiveTrackFormats = function(activeFormat, sabrFormats) {
|
|
this.checkPlayerStatus();
|
|
|
|
// Check if FormatKeyUtils is available
|
|
if (typeof FormatKeyUtils === 'undefined' || !FormatKeyUtils.getUniqueFormatId) {
|
|
return { videoFormat: undefined, audioFormat: undefined };
|
|
}
|
|
|
|
var activeVariant = this.player.getVariantTracks().find(function(track) {
|
|
return FormatKeyUtils.getUniqueFormatId(activeFormat) === (activeFormat.width ? track.originalVideoId : track.originalAudioId);
|
|
});
|
|
|
|
if (!activeVariant) {
|
|
return { videoFormat: undefined, audioFormat: undefined };
|
|
}
|
|
|
|
var formatMap = new Map(sabrFormats.map(function(format) {
|
|
return [FormatKeyUtils.getUniqueFormatId(format), format];
|
|
}));
|
|
|
|
return {
|
|
videoFormat: activeVariant.originalVideoId ? formatMap.get(activeVariant.originalVideoId) : undefined,
|
|
audioFormat: activeVariant.originalAudioId ? formatMap.get(activeVariant.originalAudioId) : undefined
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Register request interceptor
|
|
*/
|
|
ShakaPlayerAdapter.prototype.registerRequestInterceptor = function(interceptor) {
|
|
console.log('[ShakaPlayerAdapter] registerRequestInterceptor() called');
|
|
var self = this;
|
|
this.checkPlayerStatus();
|
|
|
|
var networkingEngine = this.player.getNetworkingEngine();
|
|
if (!networkingEngine) {
|
|
console.warn('[ShakaPlayerAdapter] No networking engine available');
|
|
return;
|
|
}
|
|
console.log('[ShakaPlayerAdapter] Got networking engine, registering filter');
|
|
|
|
this.requestFilter = async function(type, request, context) {
|
|
console.log('[ShakaPlayerAdapter] Request filter called:', { type: type, uri: request.uris[0] });
|
|
|
|
// Check if this is a SEGMENT request that needs processing
|
|
// Process sabr:// URLs (need conversion) and googlevideo URLs (already converted)
|
|
var uri = request.uris[0];
|
|
var isSabrUrl = uri.startsWith('sabr://');
|
|
var isGoogleVideo = isGoogleVideoURL(uri);
|
|
|
|
if (type !== shaka.net.NetworkingEngine.RequestType.SEGMENT || (!isSabrUrl && !isGoogleVideo)) {
|
|
console.log('[ShakaPlayerAdapter] Skipping request (not segment or not sabr/googlevideo):', uri);
|
|
return;
|
|
}
|
|
|
|
console.log('[ShakaPlayerAdapter] Calling interceptor for URL:', request.uris[0]);
|
|
try {
|
|
var modifiedRequest = await interceptor({
|
|
headers: request.headers,
|
|
url: request.uris[0],
|
|
method: request.method,
|
|
segment: {
|
|
getStartTime: function() { return context?.segment?.getStartTime() ?? null; },
|
|
isInit: function() { return !context?.segment; }
|
|
},
|
|
body: request.body
|
|
});
|
|
|
|
console.log('[ShakaPlayerAdapter] Interceptor returned:', modifiedRequest);
|
|
|
|
if (modifiedRequest) {
|
|
console.log('[ShakaPlayerAdapter] Request modified:', { oldUrl: request.uris[0], newUrl: modifiedRequest.url });
|
|
request.uris = modifiedRequest.url ? [modifiedRequest.url] : request.uris;
|
|
request.method = modifiedRequest.method || request.method;
|
|
request.headers = modifiedRequest.headers || request.headers;
|
|
request.body = modifiedRequest.body || request.body;
|
|
} else {
|
|
console.warn('[ShakaPlayerAdapter] Interceptor returned null/undefined for:', request.uris[0]);
|
|
}
|
|
} catch (error) {
|
|
console.error('[ShakaPlayerAdapter] Interceptor error:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
networkingEngine.registerRequestFilter(this.requestFilter);
|
|
console.log('[ShakaPlayerAdapter] Request filter registered');
|
|
};
|
|
|
|
/**
|
|
* Register response interceptor
|
|
*/
|
|
ShakaPlayerAdapter.prototype.registerResponseInterceptor = function(interceptor) {
|
|
var self = this;
|
|
this.checkPlayerStatus();
|
|
|
|
var networkingEngine = this.player.getNetworkingEngine();
|
|
if (!networkingEngine) return;
|
|
|
|
this.responseFilter = async function(type, response, context) {
|
|
if (type !== shaka.net.NetworkingEngine.RequestType.SEGMENT || !isGoogleVideoURL(response.uri)) return;
|
|
|
|
var modifiedResponse = await interceptor({
|
|
url: response.originalRequest.uris[0],
|
|
method: response.originalRequest.method,
|
|
headers: response.headers,
|
|
data: response.data,
|
|
makeRequest: async function(url, headers) {
|
|
var retryParameters = self.player.getConfiguration().streaming.retryParameters;
|
|
var redirectRequest = shaka.net.NetworkingEngine.makeRequest([url], retryParameters);
|
|
Object.assign(redirectRequest.headers, headers);
|
|
|
|
var requestOperation = networkingEngine.request(type, redirectRequest, context);
|
|
var redirectResponse = await requestOperation.promise;
|
|
|
|
return {
|
|
url: redirectResponse.uri,
|
|
method: redirectResponse.originalRequest.method,
|
|
headers: redirectResponse.headers,
|
|
data: redirectResponse.data
|
|
};
|
|
}
|
|
});
|
|
|
|
if (modifiedResponse) {
|
|
response.data = modifiedResponse.data ?? response.data;
|
|
Object.assign(response.headers, modifiedResponse.headers);
|
|
}
|
|
};
|
|
|
|
networkingEngine.registerResponseFilter(this.responseFilter);
|
|
};
|
|
|
|
/**
|
|
* Create a Shaka response object
|
|
*/
|
|
ShakaPlayerAdapter.prototype.createShakaResponse = function(args) {
|
|
return makeResponse(
|
|
headersToGenericObject(args.response.headers),
|
|
args.arrayBuffer || new ArrayBuffer(0),
|
|
args.response.status,
|
|
args.uri,
|
|
args.response.url,
|
|
args.request,
|
|
args.requestType
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Dispose of the adapter
|
|
*/
|
|
ShakaPlayerAdapter.prototype.dispose = function() {
|
|
if (this.abortController) {
|
|
this.abortController.abort();
|
|
this.abortController = null;
|
|
}
|
|
|
|
if (this.player) {
|
|
var networkingEngine = this.player.getNetworkingEngine();
|
|
|
|
if (networkingEngine) {
|
|
if (this.requestFilter) {
|
|
networkingEngine.unregisterRequestFilter(this.requestFilter);
|
|
}
|
|
if (this.responseFilter) {
|
|
networkingEngine.unregisterResponseFilter(this.responseFilter);
|
|
}
|
|
}
|
|
|
|
shaka.net.NetworkingEngine.unregisterScheme('http');
|
|
shaka.net.NetworkingEngine.unregisterScheme('https');
|
|
|
|
this.player = null;
|
|
}
|
|
};
|
|
|
|
return ShakaPlayerAdapter;
|
|
})();
|
|
|
|
// Export for use
|
|
window.ShakaPlayerAdapter = ShakaPlayerAdapter;
|