diff --git a/src/core/segment_buffers/implementations/audio_video/audio_video_segment_buffer.ts b/src/core/segment_buffers/implementations/audio_video/audio_video_segment_buffer.ts index 60c022a4cac..2178aefc474 100644 --- a/src/core/segment_buffers/implementations/audio_video/audio_video_segment_buffer.ts +++ b/src/core/segment_buffers/implementations/audio_video/audio_video_segment_buffer.ts @@ -21,11 +21,8 @@ import { import config from "../../../../config"; import log from "../../../../log"; import { getLoggableSegmentId } from "../../../../manifest"; -import areArraysOfNumbersEqual from "../../../../utils/are_arrays_of_numbers_equal"; import assertUnreachable from "../../../../utils/assert_unreachable"; -import { toUint8Array } from "../../../../utils/byte_parsing"; import createCancellablePromise from "../../../../utils/create_cancellable_promise"; -import hashBuffer from "../../../../utils/hash_buffer"; import noop from "../../../../utils/noop"; import objectAssign from "../../../../utils/object_assign"; import TaskCanceller, { @@ -139,20 +136,26 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer { private _pendingTask : IAVSBPendingTask | null; /** - * Keep track of the of the latest init segment pushed in the linked - * SourceBuffer. + * Keep track of the unique identifier of the of the latest init segment + * pushed to the linked SourceBuffer. * - * This allows to be sure the right initialization segment is pushed before - * any chunk is. + * Such identifiers are first declared through the `declareInitSegment` + * method and the corresponding initialization segment is then pushed through + * the `pushChunk` method. + * + * Keeping track of this allows to be sure the right initialization segment is + * pushed before any chunk is. * * `null` if no initialization segment have been pushed to the * `AudioVideoSegmentBuffer` yet. */ - private _lastInitSegment : { /** The init segment itself. */ - data : Uint8Array; - /** Hash of the initSegment for fast comparison */ - hash : number; } | - null; + private _lastInitSegmentUniqueId : string | null; + + /** + * Link unique identifiers for initialization segments (as communicated by + * `declareInitSegment`) to the corresponding initialization data. + */ + private _initSegmentsMap : Map; /** * @constructor @@ -174,8 +177,9 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer { this._sourceBuffer = sourceBuffer; this._queue = []; this._pendingTask = null; - this._lastInitSegment = null; + this._lastInitSegmentUniqueId = null; this.codec = codec; + this._initSegmentsMap = new Map(); const onError = this._onPendingTaskError.bind(this); const reCheck = this._flush.bind(this); @@ -198,6 +202,20 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer { }); } + public declareInitSegment( + uniqueId : string, + initSegmentData : unknown + ) : void { + assertDataIsBufferSource(initSegmentData); + this._initSegmentsMap.set(uniqueId, initSegmentData); + } + + public freeInitSegment( + uniqueId : string + ) : void { + this._initSegmentsMap.delete(uniqueId); + } + /** * Push a chunk of the media segment given to the attached SourceBuffer, in a * FIFO queue. @@ -229,12 +247,12 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer { infos : IPushChunkInfos, cancellationSignal : CancellationSignal ) : Promise { - assertPushedDataIsBufferSource(infos); + assertDataIsBufferSource(infos.data.chunk); log.debug("AVSB: receiving order to push data to the SourceBuffer", this.bufferType, getLoggableSegmentId(infos.inventoryInfos)); return this._addToQueue({ type: SegmentBufferOperation.Push, - value: infos }, + value: infos as IPushChunkInfos }, cancellationSignal); } @@ -350,7 +368,7 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer { * @param {Event} err */ private _onPendingTaskError(err : unknown) : void { - this._lastInitSegment = null; // initialize init segment as a security + this._lastInitSegmentUniqueId = null; // initialize init segment as a security if (this._pendingTask !== null) { const error = err instanceof Error ? err : @@ -447,7 +465,7 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer { const error = e instanceof Error ? e : new Error("An unknown error occured when preparing a push operation"); - this._lastInitSegment = null; // initialize init segment as a security + this._lastInitSegmentUniqueId = null; // initialize init segment as a security nextItem.reject(error); return; } @@ -557,15 +575,17 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer { this._sourceBuffer.appendWindowEnd = appendWindow[1]; } - if (data.initSegment !== null && - (hasUpdatedSourceBufferType || !this._isLastInitSegment(data.initSegment))) + if (data.initSegmentUniqueId !== null && + (hasUpdatedSourceBufferType || + !this._isLastInitSegment(data.initSegmentUniqueId))) { // Push initialization segment before the media segment - const segmentData = data.initSegment; + const segmentData = this._initSegmentsMap.get(data.initSegmentUniqueId); + if (segmentData === undefined) { + throw new Error("Invalid initialization segment uniqueId"); + } dataToPush.push(segmentData); - const initU8 = toUint8Array(segmentData); - this._lastInitSegment = { data: initU8, - hash: hashBuffer(initU8) }; + this._lastInitSegmentUniqueId = data.initSegmentUniqueId; } if (data.chunk !== null) { @@ -576,28 +596,16 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer { } /** - * Return `true` if the given `segmentData` is the same segment than the last + * Return `true` if the given `uniqueId` is the identifier of the last * initialization segment pushed to the `AudioVideoSegmentBuffer`. - * @param {BufferSource} segmentData + * @param {string} uniqueId * @returns {boolean} */ - private _isLastInitSegment(segmentData : BufferSource) : boolean { - if (this._lastInitSegment === null) { + private _isLastInitSegment(uniqueId : string) : boolean { + if (this._lastInitSegmentUniqueId === null) { return false; } - if (this._lastInitSegment.data === segmentData) { - return true; - } - const oldInit = this._lastInitSegment.data; - if (oldInit.byteLength === segmentData.byteLength) { - const newInitU8 = toUint8Array(segmentData); - if (hashBuffer(newInitU8) === this._lastInitSegment.hash && - areArraysOfNumbersEqual(oldInit, newInitU8)) - { - return true; - } - } - return false; + return this._lastInitSegmentUniqueId === uniqueId; } } @@ -605,27 +613,20 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer { * Throw if the given input is not in the expected format. * Allows to enforce runtime type-checking as compile-time type-checking here is * difficult to enforce. - * @param {Object} pushedData + * @param {Object} data */ -function assertPushedDataIsBufferSource( - pushedData : IPushChunkInfos -) : asserts pushedData is IPushChunkInfos { +function assertDataIsBufferSource( + data : unknown +) : asserts data is BufferSource { if (__ENVIRONMENT__.CURRENT_ENV === __ENVIRONMENT__.PRODUCTION as number) { return; } - const { chunk, initSegment } = pushedData.data; if ( - typeof chunk !== "object" || - typeof initSegment !== "object" || - ( - chunk !== null && - !(chunk instanceof ArrayBuffer) && - !((chunk as ArrayBufferView).buffer instanceof ArrayBuffer) - ) || + typeof data !== "object" || ( - initSegment !== null && - !(initSegment instanceof ArrayBuffer) && - !((initSegment as ArrayBufferView).buffer instanceof ArrayBuffer) + data !== null && + !(data instanceof ArrayBuffer) && + !((data as ArrayBufferView).buffer instanceof ArrayBuffer) ) ) { throw new Error("Invalid data given to the AudioVideoSegmentBuffer"); diff --git a/src/core/segment_buffers/implementations/image/image_segment_buffer.ts b/src/core/segment_buffers/implementations/image/image_segment_buffer.ts index fe0f08e798d..d89d834b074 100644 --- a/src/core/segment_buffers/implementations/image/image_segment_buffer.ts +++ b/src/core/segment_buffers/implementations/image/image_segment_buffer.ts @@ -38,6 +38,22 @@ export default class ImageSegmentBuffer extends SegmentBuffer { this._buffered = new ManualTimeRanges(); } + /** + * @param {string} uniqueId + */ + public declareInitSegment(uniqueId : string): void { + log.warn("ISB: Declaring initialization segment for image SegmentBuffer", + uniqueId); + } + + /** + * @param {string} uniqueId + */ + public freeInitSegment(uniqueId : string): void { + log.warn("ISB: Freeing initialization segment for image SegmentBuffer", + uniqueId); + } + /** * @param {Object} data * @returns {Promise} diff --git a/src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts b/src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts index f3a7ecdfe20..ef294599e83 100644 --- a/src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts +++ b/src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts @@ -137,6 +137,22 @@ export default class HTMLTextSegmentBuffer extends SegmentBuffer { this.autoRefreshSubtitles(this._canceller.signal); } + /** + * @param {string} uniqueId + */ + public declareInitSegment(uniqueId : string): void { + log.warn("ISB: Declaring initialization segment for image SegmentBuffer", + uniqueId); + } + + /** + * @param {string} uniqueId + */ + public freeInitSegment(uniqueId : string): void { + log.warn("ISB: Freeing initialization segment for image SegmentBuffer", + uniqueId); + } + /** * Push text segment to the HTMLTextSegmentBuffer. * @param {Object} infos diff --git a/src/core/segment_buffers/implementations/text/native/native_text_segment_buffer.ts b/src/core/segment_buffers/implementations/text/native/native_text_segment_buffer.ts index 0bf209d9c85..fea7a866d94 100644 --- a/src/core/segment_buffers/implementations/text/native/native_text_segment_buffer.ts +++ b/src/core/segment_buffers/implementations/text/native/native_text_segment_buffer.ts @@ -66,6 +66,22 @@ export default class NativeTextSegmentBuffer extends SegmentBuffer { this._trackElement = trackElement; } + /** + * @param {string} uniqueId + */ + public declareInitSegment(uniqueId : string): void { + log.warn("ISB: Declaring initialization segment for image SegmentBuffer", + uniqueId); + } + + /** + * @param {string} uniqueId + */ + public freeInitSegment(uniqueId : string): void { + log.warn("ISB: Freeing initialization segment for image SegmentBuffer", + uniqueId); + } + /** * @param {Object} infos * @returns {Promise} diff --git a/src/core/segment_buffers/implementations/types.ts b/src/core/segment_buffers/implementations/types.ts index cc6d697accb..39cc1abaa49 100644 --- a/src/core/segment_buffers/implementations/types.ts +++ b/src/core/segment_buffers/implementations/types.ts @@ -87,6 +87,13 @@ export abstract class SegmentBuffer { this._segmentInventory = new SegmentInventory(); } + public abstract declareInitSegment( + uniqueId : string, + initSegmentData : unknown + ) : void; + + public abstract freeInitSegment(uniqueId : string) : void; + /** * Push a chunk of the media segment given to the attached buffer, in a * FIFO queue. @@ -96,7 +103,8 @@ export abstract class SegmentBuffer { * pushed. * * Depending on the type of data appended, the pushed chunk might rely on an - * initialization segment, given through the `data.initSegment` property. + * initialization segment, which had to be previously declared through the + * `declareInitSegment` method. * * Such initialization segment will be first pushed to the buffer if the * last pushed segment was associated to another initialization segment. @@ -106,7 +114,7 @@ export abstract class SegmentBuffer { * reference). * * If you don't need any initialization segment to push the wanted chunk, you - * can just set `data.initSegment` to `null`. + * can just set the corresponding property to `null`. * * You can also only push an initialization segment by setting the * `data.chunk` argument to null. @@ -230,12 +238,16 @@ export type IBufferType = "audio" | */ export interface IPushedChunkData { /** - * The whole initialization segment's data related to the chunk you want to + * The `uniqueId` of the initialization segment linked to the data you want to * push. + * + * That identifier should previously have been declared through the + * `declareInitSegment` method and not freed. + * * To set to `null` either if no initialization data is needed, or if you are * confident that the last pushed one is compatible. */ - initSegment: T | null; + initSegmentUniqueId : string | null; /** * Chunk you want to push. * This can be the whole decodable segment's data or just a decodable sub-part diff --git a/src/core/stream/representation/representation_stream.ts b/src/core/stream/representation/representation_stream.ts index a508ca4ef81..692b38c0fa6 100644 --- a/src/core/stream/representation/representation_stream.ts +++ b/src/core/stream/representation/representation_stream.ts @@ -109,11 +109,17 @@ export default function RepresentationStream( segmentsLoadingCanceller.linkToSignal(globalCanceller.signal); /** Saved initialization segment state for this representation. */ - const initSegmentState : IInitSegmentState = { + const initSegmentState : IInitSegmentState = { segment: representation.index.getInitSegment(), - segmentData: null, + uniqueId: null, isLoaded: false, }; + globalCanceller.signal.register(() => { + // Free initialization segment if one has been declared + if (initSegmentState.uniqueId !== null) { + segmentBuffer.freeInitSegment(initSegmentState.uniqueId); + } + }); /** Emit the last scheduled downloading queue for segments. */ const lastSegmentQueue = createSharedReference({ @@ -125,7 +131,6 @@ export default function RepresentationStream( const hasInitSegment = initSegmentState.segment !== null; if (!hasInitSegment) { - initSegmentState.segmentData = null; initSegmentState.isLoaded = true; } @@ -338,7 +343,6 @@ export default function RepresentationStream( return ; } if (evt.segmentType === "init") { - initSegmentState.segmentData = evt.initializationData; initSegmentState.isLoaded = true; // Now that the initialization segment has been parsed - which may have @@ -350,21 +354,31 @@ export default function RepresentationStream( callbacks.encryptionDataEncountered( allEncryptionData.map(p => objectAssign({ content }, p)) ); + if (globalCanceller.isUsed()) { + return ; // previous callback has stopped everything by side-effect + } } } - pushInitSegment({ playbackObserver, - content, - segment: evt.segment, - segmentData: evt.initializationData, - segmentBuffer }, - globalCanceller.signal) - .then((result) => { - if (result !== null) { - callbacks.addedSegment(result); - } - }) - .catch(onFatalBufferError); + if (evt.initializationData !== null) { + const initSegmentUniqueId = representation.uniqueId; + initSegmentState.uniqueId = initSegmentUniqueId; + segmentBuffer.declareInitSegment(initSegmentUniqueId, + evt.initializationData); + pushInitSegment({ playbackObserver, + content, + initSegmentUniqueId, + segment: evt.segment, + segmentData: evt.initializationData, + segmentBuffer }, + globalCanceller.signal) + .then((result) => { + if (result !== null) { + callbacks.addedSegment(result); + } + }) + .catch(onFatalBufferError); + } // Sometimes the segment list is only known once the initialization segment // is parsed. Thus we immediately re-check if there's new segments to load. @@ -401,10 +415,10 @@ export default function RepresentationStream( } } - const initSegmentData = initSegmentState.segmentData; + const initSegmentUniqueId = initSegmentState.uniqueId; pushMediaSegment({ playbackObserver, content, - initSegmentData, + initSegmentUniqueId, parsedSegment: evt, segment: evt.segment, segmentBuffer }, @@ -440,17 +454,18 @@ export default function RepresentationStream( * Information about the initialization segment linked to the Representation * which the RepresentationStream try to download segments for. */ -interface IInitSegmentState { +interface IInitSegmentState { /** * Segment Object describing that initialization segment. * `null` if there's no initialization segment for that Representation. */ segment : ISegment | null; /** - * Initialization segment data. - * `null` either when it doesn't exist or when it has not been loaded yet. + * Unique identifier used to identify the initialization segment data, used by + * the `SegmentBuffer`. + * `null` either when it doesn't exist or when it has not been declared yet. */ - segmentData : T | null; + uniqueId : string | null; /** `true` if the initialization segment has been loaded and parsed. */ isLoaded : boolean; } diff --git a/src/core/stream/representation/utils/push_init_segment.ts b/src/core/stream/representation/utils/push_init_segment.ts index c0fecd995a5..a2ba790ab25 100644 --- a/src/core/stream/representation/utils/push_init_segment.ts +++ b/src/core/stream/representation/utils/push_init_segment.ts @@ -43,6 +43,7 @@ export default async function pushInitSegment( { playbackObserver, content, + initSegmentUniqueId, segment, segmentData, segmentBuffer, @@ -54,20 +55,18 @@ export default async function pushInitSegment( manifest : Manifest; period : Period; representation : Representation; }; - segmentData : T | null; + initSegmentUniqueId : string; + segmentData : T; segment : ISegment; segmentBuffer : SegmentBuffer; }, cancelSignal : CancellationSignal ) : Promise< IStreamEventAddedSegmentPayload | null > { - if (segmentData === null) { - return null; - } if (cancelSignal.cancellationError !== null) { throw cancelSignal.cancellationError; } const codec = content.representation.getMimeTypeString(); - const data : IPushedChunkData = { initSegment: segmentData, + const data : IPushedChunkData = { initSegmentUniqueId, chunk: null, timestampOffset: 0, appendWindow: [ undefined, undefined ], diff --git a/src/core/stream/representation/utils/push_media_segment.ts b/src/core/stream/representation/utils/push_media_segment.ts index 396a0b03689..ac9e45e2775 100644 --- a/src/core/stream/representation/utils/push_media_segment.ts +++ b/src/core/stream/representation/utils/push_media_segment.ts @@ -41,7 +41,7 @@ import appendSegmentToBuffer from "./append_segment_to_buffer"; export default async function pushMediaSegment( { playbackObserver, content, - initSegmentData, + initSegmentUniqueId, parsedSegment, segment, segmentBuffer } : @@ -52,7 +52,7 @@ export default async function pushMediaSegment( manifest : Manifest; period : Period; representation : Representation; }; - initSegmentData : T | null; + initSegmentUniqueId : string | null; parsedSegment : ISegmentParserParsedMediaChunk; segment : ISegment; segmentBuffer : SegmentBuffer; }, @@ -83,7 +83,7 @@ export default async function pushMediaSegment( undefined, ]; - const data = { initSegment: initSegmentData, + const data = { initSegmentUniqueId, chunk: chunkData, timestampOffset: chunkOffset, appendWindow: safeAppendWindow, diff --git a/src/experimental/tools/VideoThumbnailLoader/load_and_push_segment.ts b/src/experimental/tools/VideoThumbnailLoader/load_and_push_segment.ts index 4fa77fcf530..407af6c2929 100644 --- a/src/experimental/tools/VideoThumbnailLoader/load_and_push_segment.ts +++ b/src/experimental/tools/VideoThumbnailLoader/load_and_push_segment.ts @@ -33,30 +33,33 @@ export default function loadAndPushSegment( segmentInfo : ISegmentLoaderContent, segmentBuffer: AudioVideoSegmentBuffer, segmentFetcher: ISegmentFetcher, + initSegmentUniqueId : string | null, cancelSignal: CancellationSignal ): Promise { const pushOperations : Array> = []; return segmentFetcher(segmentInfo, { onChunk(parseChunk) { const parsed = parseChunk(undefined); - let isIsInitSegment : boolean; + let isInitSegment : boolean; let data : BufferSource | null; let timestampOffset : number; const codec = segmentInfo.representation.getMimeTypeString(); if (parsed.segmentType === "init") { - isIsInitSegment = true; + isInitSegment = true; data = parsed.initializationData; timestampOffset = 0; + if (initSegmentUniqueId !== null) { + segmentBuffer.declareInitSegment(initSegmentUniqueId, data); + } } else { - isIsInitSegment = false; + isInitSegment = false; data = parsed.chunkData; timestampOffset = parsed.chunkOffset; } const pushOperation = segmentBuffer.pushChunk({ - data: { initSegment: isIsInitSegment ? data : - null, - chunk: isIsInitSegment ? null : - data, + data: { initSegmentUniqueId, + chunk: isInitSegment ? null : + data, appendWindow: [segmentInfo.period.start, segmentInfo.period.end], timestampOffset, codec }, diff --git a/src/experimental/tools/VideoThumbnailLoader/video_thumbnail_loader.ts b/src/experimental/tools/VideoThumbnailLoader/video_thumbnail_loader.ts index 17d389011dd..50596f4c492 100644 --- a/src/experimental/tools/VideoThumbnailLoader/video_thumbnail_loader.ts +++ b/src/experimental/tools/VideoThumbnailLoader/video_thumbnail_loader.ts @@ -166,7 +166,7 @@ export default class VideoThumbnailLoader { let lastRepInfo : IVideoThumbnailLoaderRepresentationInfo; if (this._lastRepresentationInfo === null) { - const cleaner = new TaskCanceller(); + const lastRepInfoCleaner = new TaskCanceller(); const segmentFetcher = createSegmentFetcher( "video", loader.video, @@ -179,13 +179,17 @@ export default class VideoThumbnailLoader { maxRetryRegular: 0, requestTimeout: config.getCurrent().DEFAULT_REQUEST_TIMEOUT } ) as ISegmentFetcher; + const initSegment = content.representation.index.getInitSegment(); + const initSegmentUniqueId = initSegment !== null ? + content.representation.uniqueId : + null; const segmentBufferProm = prepareSourceBuffer( this._videoElement, content.representation.getMimeTypeString(), - cleaner.signal + lastRepInfoCleaner.signal ).then(async (segmentBuffer) => { - const initSegment = content.representation.index.getInitSegment(); - if (initSegment === null) { + if (initSegment === null || initSegmentUniqueId === null) { + lastRepInfo.initSegmentUniqueId = null; return segmentBuffer; } const segmentInfo = objectAssign({ segment: initSegment }, @@ -193,13 +197,18 @@ export default class VideoThumbnailLoader { await loadAndPushSegment(segmentInfo, segmentBuffer, lastRepInfo.segmentFetcher, - cleaner.signal); + initSegmentUniqueId, + lastRepInfoCleaner.signal); + lastRepInfoCleaner.signal.register(() => { + segmentBuffer.freeInitSegment(initSegmentUniqueId); + }); return segmentBuffer; }); lastRepInfo = { - cleaner, + cleaner: lastRepInfoCleaner, segmentBuffer: segmentBufferProm, content, + initSegmentUniqueId, segmentFetcher, pendingRequests: [], }; @@ -251,6 +260,7 @@ export default class VideoThumbnailLoader { const prom = loadAndPushSegment(segmentInfo, segmentBuffer, lastRepInfo.segmentFetcher, + lastRepInfo.initSegmentUniqueId, requestCanceller.signal) .then(unlinkSignal, (err) => { unlinkSignal(); @@ -390,6 +400,7 @@ interface IVideoThumbnailLoaderRepresentationInfo { * `pendingRequests`. */ pendingRequests : IPendingRequestInfo[]; + initSegmentUniqueId : string | null; } interface IPendingRequestInfo { diff --git a/src/manifest/__tests__/manifest.test.ts b/src/manifest/__tests__/manifest.test.ts index 9faaa611ecf..935b2707560 100644 --- a/src/manifest/__tests__/manifest.test.ts +++ b/src/manifest/__tests__/manifest.test.ts @@ -72,7 +72,7 @@ describe("Manifest - Manifest", () => { expect(manifest.suggestedPresentationDelay).toEqual(undefined); expect(manifest.uris).toEqual([]); - expect(fakeIdGenerator).toHaveBeenCalledTimes(2); + expect(fakeIdGenerator).toHaveBeenCalled(); expect(fakeGenerateNewId).toHaveBeenCalledTimes(1); expect(fakeLogger.info).not.toHaveBeenCalled(); expect(fakeLogger.warn).not.toHaveBeenCalled(); @@ -114,7 +114,7 @@ describe("Manifest - Manifest", () => { contentWarnings: [] } ]); expect(manifest.adaptations).toEqual({}); - expect(fakeIdGenerator).toHaveBeenCalledTimes(2); + expect(fakeIdGenerator).toHaveBeenCalled(); expect(fakeGenerateNewId).toHaveBeenCalledTimes(1); expect(fakeLogger.info).not.toHaveBeenCalled(); expect(fakeLogger.warn).not.toHaveBeenCalled(); @@ -152,7 +152,7 @@ describe("Manifest - Manifest", () => { expect(fakePeriod).toHaveBeenCalledTimes(2); expect(fakePeriod).toHaveBeenCalledWith(period1, representationFilter); expect(fakePeriod).toHaveBeenCalledWith(period2, representationFilter); - expect(fakeIdGenerator).toHaveBeenCalledTimes(2); + expect(fakeIdGenerator).toHaveBeenCalled(); expect(fakeGenerateNewId).toHaveBeenCalledTimes(1); expect(fakeLogger.info).not.toHaveBeenCalled(); expect(fakeLogger.warn).not.toHaveBeenCalled(); @@ -194,7 +194,7 @@ describe("Manifest - Manifest", () => { ]); expect(manifest.adaptations).toBe(adapP1); - expect(fakeIdGenerator).toHaveBeenCalledTimes(2); + expect(fakeIdGenerator).toHaveBeenCalled(); expect(fakeGenerateNewId).toHaveBeenCalledTimes(1); expect(fakeLogger.info).not.toHaveBeenCalled(); expect(fakeLogger.warn).not.toHaveBeenCalled(); @@ -234,7 +234,7 @@ describe("Manifest - Manifest", () => { expect(manifest.contentWarnings).toContainEqual(new Error("0")); expect(manifest.contentWarnings).toContainEqual(new Error("1")); - expect(fakeIdGenerator).toHaveBeenCalledTimes(2); + expect(fakeIdGenerator).toHaveBeenCalled(); expect(fakeGenerateNewId).toHaveBeenCalledTimes(1); expect(fakeLogger.info).not.toHaveBeenCalled(); expect(fakeLogger.warn).not.toHaveBeenCalled(); @@ -285,7 +285,7 @@ describe("Manifest - Manifest", () => { ]); expect(manifest.suggestedPresentationDelay).toEqual(99); expect(manifest.uris).toEqual(["url1", "url2"]); - expect(fakeIdGenerator).toHaveBeenCalledTimes(2); + expect(fakeIdGenerator).toHaveBeenCalled(); expect(fakeGenerateNewId).toHaveBeenCalledTimes(1); expect(fakeLogger.info).not.toHaveBeenCalled(); expect(fakeLogger.warn).not.toHaveBeenCalled(); @@ -419,7 +419,7 @@ describe("Manifest - Manifest", () => { .toHaveBeenCalledWith(manifest.periods, newManifest.periods); expect(mockTrigger).toHaveBeenCalledTimes(1); expect(mockTrigger).toHaveBeenCalledWith("manifestUpdate", fakeReplacePeriodsRes); - expect(fakeIdGenerator).toHaveBeenCalledTimes(2); + expect(fakeIdGenerator).toHaveBeenCalled(); expect(fakeGenerateNewId).toHaveBeenCalledTimes(1); expect(fakeLogger.info).not.toHaveBeenCalled(); expect(fakeLogger.warn).not.toHaveBeenCalled(); diff --git a/src/manifest/representation.ts b/src/manifest/representation.ts index 3976c03ea97..bb371353e55 100644 --- a/src/manifest/representation.ts +++ b/src/manifest/representation.ts @@ -27,19 +27,44 @@ import { IVideoRepresentation, } from "../public_types"; import areArraysOfNumbersEqual from "../utils/are_arrays_of_numbers_equal"; +import idGenerator from "../utils/id_generator"; import { IRepresentationIndex } from "./representation_index"; import { IAdaptationType, } from "./types"; +const generateRepresentationUniqueId = idGenerator(); + /** * Normalized Representation structure. * @class Representation */ class Representation { - /** ID uniquely identifying the Representation in the Adaptation. */ + /** + * ID uniquely identifying the `Representation` in its parent `Adaptation`. + * + * This identifier might be linked to an identifier present in the original + * Manifest file, it is thus the identifier to use to determine if a + * `Representation` from a refreshed `Manifest` is actually the same one than + * one in the previously loaded Manifest (as long as the `Adaptation` and + * `Period` are also the same). + * + * For a globally unique identifier regardless of the `Adaptation`, `Period` + * or even `Manifest`, you can rely on `uniqueId` instead. + */ public readonly id : string; + /** + * Globally unique identifier for this `Representation` object. + * + * This identifier is guaranteed to be unique for any `Representation`s of all + * `Manifest` objects created in the current JS Realm. + * As such, it can be used as an identifier for the JS object itself, whereas + * `id` is the identifier for the original Manifest's Representation in the + * scope of its parent `Adaptation`. + */ + public readonly uniqueId : string; + /** * Interface allowing to get information about segments available for this * Representation. @@ -119,6 +144,7 @@ class Representation { */ constructor(args : IParsedRepresentation, opts : { type : IAdaptationType }) { this.id = args.id; + this.uniqueId = generateRepresentationUniqueId(); this.bitrate = args.bitrate; this.codec = args.codecs; diff --git a/src/manifest/utils.ts b/src/manifest/utils.ts index cc76fdd8750..c73fc49b248 100644 --- a/src/manifest/utils.ts +++ b/src/manifest/utils.ts @@ -37,9 +37,7 @@ export function areSameContent( content2: IBufferedChunkInfos ): boolean { return (content1.segment.id === content2.segment.id && - content1.representation.id === content2.representation.id && - content1.adaptation.id === content2.adaptation.id && - content1.period.id === content2.period.id); + content1.representation.uniqueId === content2.representation.uniqueId); } /** diff --git a/src/tools/TextTrackRenderer/text_track_renderer.ts b/src/tools/TextTrackRenderer/text_track_renderer.ts index 2638aa30fbe..c60ba4f75e8 100644 --- a/src/tools/TextTrackRenderer/text_track_renderer.ts +++ b/src/tools/TextTrackRenderer/text_track_renderer.ts @@ -78,7 +78,7 @@ export default class TextTrackRenderer { args.timeOffset : 0; this._segmentBuffer.pushChunkSync({ inventoryInfos: null, - data: { initSegment: null, + data: { initSegmentUniqueId: null, codec: args.type, timestampOffset, appendWindow: [0, Infinity],