UNPKG

p2p-media-loader-shaka

Version:

P2P Media Loader Shaka Player integration

295 lines 13.2 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { debug, generateStreamShortId, } from "p2p-media-loader-core"; const AUDIO_CODECS = ["mp4a", "ac-3", "ec-3", "ec+3", "opus", "vorb", "flac"]; 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"; } start(uri, playerInterface) { return __awaiter(this, void 0, void 0, function* () { const { p2pml } = playerInterface.networkingEngine; this.setP2PMediaLoaderData(p2pml); const manifest = yield 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) { var _a, _b; const { segmentManager } = this; if (!segmentManager) return; const processedStreams = new Set(); const processStream = (stream, type, index) => { this.hookSegmentIndex(stream); segmentManager.setStream(stream, type, index); processedStreams.add(stream.id); return true; }; for (const variant of variants) { const { video, audio } = variant; if (video && !processedStreams.has(video.id)) { const isMissingMetadata = variant.bandwidth === 0; // In muxed streams, Shaka natively includes audio codecs in the video codec array. // We strip standard audio prefixes here to strictly match HLS.js's cleanly separated // videoCodec parsing, ensuring peers on identical video tracks share P2P segments // regardless of differently selected audio track descriptors. const videoCodecs = video.codecs ? video.codecs .split(",") .map((c) => c.trim().toLowerCase()) .filter((c) => !AUDIO_CODECS.some((p) => c.startsWith(p))) .join(",") : undefined; const { frameRate, hdr: videoRange } = video; const index = generateStreamShortId({ bitrate: variant.bandwidth, codecs: isMissingMetadata ? undefined : videoCodecs, width: isMissingMetadata ? undefined : video.width, height: isMissingMetadata ? undefined : video.height, frameRate: isMissingMetadata ? undefined : frameRate, videoRange: isMissingMetadata ? undefined : videoRange, }); processStream(video, "main", index); } if (audio && !processedStreams.has(audio.id)) { const isMain = !video; // audio-only master playlist variants const name = (_b = (_a = audio.label) !== null && _a !== void 0 ? _a : audio.originalId) !== null && _b !== void 0 ? _b : undefined; const index = generateStreamShortId({ bitrate: isMain ? variant.bandwidth : 0, codecs: isMain ? undefined : audio.codecs, language: isMain ? undefined : audio.language, channels: isMain ? undefined : audio.channelsCount, name: isMain ? undefined : name, }); processStream(audio, isMain ? "main" : "secondary", index); } } } 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) => { var _a, _b; const reference = originalGet.call(segmentIndex, position); if (reference === prevReference || (!((_a = this.player) === null || _a === void 0 ? void 0 : _a.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 (_c) { // 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 || !!((_b = this.player) === null || _b === void 0 ? void 0 : _b.isLive()) || !callFromCreateSegmentIndexMethod) { segmentIndex.get = customGet; } } return reference; }; segmentIndex.get = customGet; }; if (stream.segmentIndex) { substituteSegmentIndexGet(stream.segmentIndex); return; } const createSegmentIndexOriginal = stream.createSegmentIndex; stream.createSegmentIndex = () => __awaiter(this, void 0, void 0, function* () { const result = yield createSegmentIndexOriginal.call(stream); if (stream.segmentIndex) { substituteSegmentIndexGet(stream.segmentIndex, true); } return result; }); } hookHlsStreamMediaSequenceTimeMaps(variants) { var _a; 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 = audioMap; } } return; } // For version 4.2; Retrieving mediaSequence map for each HLS playlist const manifestVariantsMap = maps.find((map) => { var _a; 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" && ((_a = item === null || item === void 0 ? void 0 : item.streams) === null || _a === void 0 ? void 0 : _a.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 ((_a = variant === null || variant === void 0 ? void 0 : variant.stream) === null || _a === void 0 ? void 0 : _a.mediaSequenceTimeMap) continue; const mediaSequenceTimeMap = getMapPropertiesFromObject(variant).find((map) => { var _a; const [key, value] = (_a = map.entries().next().value) !== null && _a !== void 0 ? _a : []; 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) { // TODO: rewrite to Object.values once the project is migrated to ES2017+ return Object.keys(object) .map((key) => object[key]) .filter((property) => property instanceof Map); } //# sourceMappingURL=manifest-parser-decorator.js.map