p2p-media-loader-shaka
Version:
P2P Media Loader Shaka Player integration
247 lines • 9.8 kB
JavaScript
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