UNPKG

p2p-media-loader-shaka

Version:

P2P Media Loader Shaka Player integration

247 lines 9.8 kB
import { debug } from "p2p-media-loader-core"; export class ManifestParserDecorator { constructor(shaka, originalManifestParser) { Object.defineProperty(this, "shaka", { enumerable: true, configurable: true, writable: true, value: shaka }); Object.defineProperty(this, "originalManifestParser", { enumerable: true, configurable: true, writable: true, value: originalManifestParser }); Object.defineProperty(this, "debug", { enumerable: true, configurable: true, writable: true, value: debug("p2pml-shaka:manifest-parser") }); Object.defineProperty(this, "isHls", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "segmentManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "player", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.isHls = this.originalManifestParser instanceof shaka.hls.HlsParser; } configure(config) { return this.originalManifestParser.configure(config); } banLocation(uri) { return this.originalManifestParser.banLocation(uri); } onInitialVariantChosen(variant) { return this.originalManifestParser.onInitialVariantChosen(variant); } setP2PMediaLoaderData(p2pml) { if (!p2pml) return; this.segmentManager = p2pml.segmentManager; this.player = p2pml.player; p2pml.streamInfo.protocol = this.isHls ? "hls" : "dash"; } async start(uri, playerInterface) { const { p2pml } = playerInterface.networkingEngine; this.setP2PMediaLoaderData(p2pml); const manifest = await this.originalManifestParser.start(uri, playerInterface); if (!p2pml) return manifest; if (this.isHls) { this.hookHlsStreamMediaSequenceTimeMaps(manifest.variants); } this.processStreams(manifest.variants); return manifest; } stop() { return this.originalManifestParser.stop(); } update() { return this.originalManifestParser.update(); } setMediaElement(mediaElement) { return this.originalManifestParser.setMediaElement(mediaElement); } onExpirationUpdated(sessionId, expiration) { return this.originalManifestParser.onExpirationUpdated(sessionId, expiration); } processStreams(variants) { const { segmentManager } = this; if (!segmentManager) return; const processedStreams = new Set(); const processStream = (stream, type, order) => { this.hookSegmentIndex(stream); segmentManager.setStream(stream, type, order); processedStreams.add(stream.id); return true; }; let videoCount = 0; let audioCount = 0; for (const variant of variants) { const { video, audio } = variant; if (video && !processedStreams.has(video.id)) { processStream(video, "main", videoCount++); } if (audio && !processedStreams.has(audio.id)) { processStream(audio, !video ? "main" : "secondary", audioCount++); } } } hookSegmentIndex(stream) { const { segmentManager } = this; if (!segmentManager) return; const substituteSegmentIndexGet = (segmentIndex, callFromCreateSegmentIndexMethod = false) => { let prevReference = null; let prevFirstItemReference; let prevLastItemReference; // eslint-disable-next-line @typescript-eslint/unbound-method const originalGet = segmentIndex.get; const customGet = (position) => { const reference = originalGet.call(segmentIndex, position); if (reference === prevReference || (!this.player?.isLive() && stream.isSegmentIndexAlreadyRead)) { return reference; } prevReference = reference; segmentIndex.get = originalGet; try { const references = getReferencesArray(segmentIndex, this.shaka); if (!references) { throw new Error("Segment references not found"); } const firstItemReference = references[0]; const lastItemReference = references[references.length - 1]; if (firstItemReference === prevFirstItemReference && lastItemReference === prevLastItemReference) { return reference; } prevFirstItemReference = firstItemReference; prevLastItemReference = lastItemReference; // Segment index have been updated segmentManager.updateStreamSegments(stream, references); stream.isSegmentIndexAlreadyRead = true; this.debug(`Stream ${stream.id} is updated`); } catch { // Ignore an error when segmentIndex inner array is empty } finally { // Do not set custom get again if the segment index is already read and the stream is VOD if (!stream.isSegmentIndexAlreadyRead || !!this.player?.isLive() || !callFromCreateSegmentIndexMethod) { segmentIndex.get = customGet; } } return reference; }; segmentIndex.get = customGet; }; if (stream.segmentIndex) { substituteSegmentIndexGet(stream.segmentIndex); return; } const createSegmentIndexOriginal = stream.createSegmentIndex; stream.createSegmentIndex = async () => { const result = await createSegmentIndexOriginal.call(stream); if (stream.segmentIndex) { substituteSegmentIndexGet(stream.segmentIndex, true); } return result; }; } hookHlsStreamMediaSequenceTimeMaps(variants) { const maps = getMapPropertiesFromObject(this.originalManifestParser); // For version 4.3 and above let videoMap; let audioMap; const keysToCheck = ["video", "audio", "text", "image"]; for (const map of maps) { if (!keysToCheck.every((key) => map.has(key))) continue; videoMap = map.get("video"); audioMap = map.get("audio"); } if (videoMap && audioMap) { for (const variant of variants) { const { video: videoStream, audio: audioStream } = variant; if (videoStream) { videoStream.mediaSequenceTimeMap = videoMap; } if (audioStream) { audioStream.mediaSequenceTimeMap = videoMap; } } return; } // For version 4.2; Retrieving mediaSequence map for each HLS playlist const manifestVariantsMap = maps.find((map) => { const item = map.values().next().value; return ( // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access typeof item === "object" && item?.streams?.createSegmentIndex); }); if (!manifestVariantsMap) return; const manifestVariantMapValues = [...manifestVariantsMap.values()]; for (const variant of manifestVariantMapValues) { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access if (variant?.stream?.mediaSequenceTimeMap) continue; const mediaSequenceTimeMap = getMapPropertiesFromObject(variant).find((map) => { const [key, value] = map.entries().next().value ?? []; return typeof key === "number" && typeof value === "number"; }); if (!mediaSequenceTimeMap) continue; // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access variant.stream.mediaSequenceTimeMap = mediaSequenceTimeMap; } } } export class HlsManifestParser extends ManifestParserDecorator { constructor(shaka) { super(shaka, new shaka.hls.HlsParser()); } } export class DashManifestParser extends ManifestParserDecorator { constructor(shaka) { super(shaka, new shaka.dash.DashParser()); } } function getReferencesArray(obj, shaka) { for (const key in obj) { if (Array.isArray(obj[key]) && obj[key].length > 0 && obj[key][0] instanceof shaka.media.SegmentReference) { return obj[key]; } else if (typeof obj[key] === "object") { const references = getReferencesArray(obj[key], shaka); if (references) return references; } } return null; } function getMapPropertiesFromObject(object) { return Object.values(object).filter((property) => property instanceof Map); } //# sourceMappingURL=manifest-parser-decorator.js.map