import type { MockInstance } from "vitest"; import { describe, beforeEach, afterEach, it, expect, vi } from "vitest"; import log from "../../../log"; import type Period from "../period"; import { MANIFEST_UPDATE_TYPE } from "../types"; import updatePeriodInPlace from "../update_period_in_place"; const oldVideoRepresentation1 = { id: "rep-video-1", getMetadataSnapshot() { return { id: "rep-video-1", bitrate: 5 }; }, index: { _update() { /* noop */ }, _replace() { /* noop */ }, }, }; const oldVideoRepresentation2 = { id: "rep-video-2", getMetadataSnapshot() { return { id: "rep-video-2", bitrate: 6 }; }, index: { _update() { /* noop */ }, _replace() { /* noop */ }, }, }; const oldVideoRepresentation3 = { id: "rep-video-3", getMetadataSnapshot() { return { id: "rep-video-3", bitrate: 7 }; }, index: { _update() { /* noop */ }, _replace() { /* noop */ }, }, }; const oldVideoRepresentation4 = { id: "rep-video-4", getMetadataSnapshot() { return { id: "rep-video-4", bitrate: 8 }; }, index: { _update() { /* noop */ }, _replace() { /* noop */ }, }, }; const oldAudioRepresentation = { id: "rep-audio-1", getMetadataSnapshot() { return { id: "rep-audio-1", bitrate: 65 }; }, index: { _update() { /* noop */ }, _replace() { /* noop */ }, }, }; const newVideoRepresentation1 = { id: "rep-video-1", getMetadataSnapshot() { return { id: "rep-video-2", bitrate: 11 }; }, index: { _update() { /* noop */ }, _replace() { /* noop */ }, }, }; const newVideoRepresentation2 = { id: "rep-video-2", getMetadataSnapshot() { return { id: "rep-video-2", bitrate: 12 }; }, index: { _update() { /* noop */ }, _replace() { /* noop */ }, }, }; const newVideoRepresentation3 = { id: "rep-video-3", getMetadataSnapshot() { return { id: "rep-video-3", bitrate: 13 }; }, index: { _update() { /* noop */ }, _replace() { /* noop */ }, }, }; const newVideoRepresentation4 = { id: "rep-video-4", getMetadataSnapshot() { return { id: "rep-video-4", bitrate: 14 }; }, index: { _update() { /* noop */ }, _replace() { /* noop */ }, }, }; const newAudioRepresentation = { id: "rep-audio-1", getMetadataSnapshot() { return { id: "rep-audio-1", bitrate: 69 }; }, index: { _update() { /* noop */ }, _replace() { /* noop */ }, }, }; function generateFakeAdaptation({ type, id, representations, }: { type: string; id: string; representations: unknown[]; }) { return { id, type, getMetadataSnapshot() { return { id, type, representations }; }, representations, }; } function generateFakeThumbnailTrack({ id }: { id: string }) { return { id, mimeType: "image/png", height: 100, width: 200, horizontalTiles: 5, verticalTiles: 3, start: 0, end: 100, tileDuration: 2, index: { _update() { /* noop */ }, _replace() { /* noop */ }, }, }; } describe("Manifest - updatePeriodInPlace", () => { let mockOldVideoRepresentation1Replace: MockInstance | undefined; let mockOldVideoRepresentation2Replace: MockInstance | undefined; let mockOldVideoRepresentation3Replace: MockInstance | undefined; let mockOldVideoRepresentation4Replace: MockInstance | undefined; let mockOldAudioRepresentationReplace: MockInstance | undefined; let mockOldVideoRepresentation1Update: MockInstance | undefined; let mockOldVideoRepresentation2Update: MockInstance | undefined; let mockOldVideoRepresentation3Update: MockInstance | undefined; let mockOldVideoRepresentation4Update: MockInstance | undefined; let mockOldAudioRepresentationUpdate: MockInstance | undefined; let mockNewVideoRepresentation1Replace: MockInstance | undefined; let mockNewVideoRepresentation2Replace: MockInstance | undefined; let mockNewVideoRepresentation3Replace: MockInstance | undefined; let mockNewVideoRepresentation4Replace: MockInstance | undefined; let mockNewAudioRepresentationReplace: MockInstance | undefined; let mockNewVideoRepresentation1Update: MockInstance | undefined; let mockNewVideoRepresentation2Update: MockInstance | undefined; let mockNewVideoRepresentation3Update: MockInstance | undefined; let mockNewVideoRepresentation4Update: MockInstance | undefined; let mockNewAudioRepresentationUpdate: MockInstance | undefined; beforeEach(() => { mockOldVideoRepresentation1Replace = vi.spyOn( oldVideoRepresentation1.index, "_replace", ); mockOldVideoRepresentation2Replace = vi.spyOn( oldVideoRepresentation2.index, "_replace", ); mockOldVideoRepresentation3Replace = vi.spyOn( oldVideoRepresentation3.index, "_replace", ); mockOldVideoRepresentation4Replace = vi.spyOn( oldVideoRepresentation4.index, "_replace", ); mockOldAudioRepresentationReplace = vi.spyOn( oldAudioRepresentation.index, "_replace", ); mockOldVideoRepresentation1Update = vi.spyOn( oldVideoRepresentation1.index, "_update", ); mockOldVideoRepresentation2Update = vi.spyOn( oldVideoRepresentation2.index, "_update", ); mockOldVideoRepresentation3Update = vi.spyOn( oldVideoRepresentation3.index, "_update", ); mockOldVideoRepresentation4Update = vi.spyOn( oldVideoRepresentation4.index, "_update", ); mockOldAudioRepresentationUpdate = vi.spyOn(oldAudioRepresentation.index, "_update"); mockNewVideoRepresentation1Replace = vi.spyOn( newVideoRepresentation1.index, "_replace", ); mockNewVideoRepresentation2Replace = vi.spyOn( newVideoRepresentation2.index, "_replace", ); mockNewVideoRepresentation3Replace = vi.spyOn( newVideoRepresentation3.index, "_replace", ); mockNewVideoRepresentation4Replace = vi.spyOn( newVideoRepresentation4.index, "_replace", ); mockNewAudioRepresentationReplace = vi.spyOn( newAudioRepresentation.index, "_replace", ); mockNewVideoRepresentation1Update = vi.spyOn( newVideoRepresentation1.index, "_update", ); mockNewVideoRepresentation2Update = vi.spyOn( newVideoRepresentation2.index, "_update", ); mockNewVideoRepresentation3Update = vi.spyOn( newVideoRepresentation3.index, "_update", ); mockNewVideoRepresentation4Update = vi.spyOn( newVideoRepresentation4.index, "_update", ); mockNewAudioRepresentationUpdate = vi.spyOn(newAudioRepresentation.index, "_update"); }); afterEach(() => { mockOldVideoRepresentation1Replace?.mockRestore(); mockOldVideoRepresentation2Replace?.mockRestore(); mockOldVideoRepresentation3Replace?.mockRestore(); mockOldVideoRepresentation4Replace?.mockRestore(); mockOldAudioRepresentationReplace?.mockRestore(); mockOldVideoRepresentation1Update?.mockRestore(); mockOldVideoRepresentation2Update?.mockRestore(); mockOldVideoRepresentation3Update?.mockRestore(); mockOldVideoRepresentation4Update?.mockRestore(); mockOldAudioRepresentationUpdate?.mockRestore(); mockNewVideoRepresentation1Replace?.mockRestore(); mockNewVideoRepresentation2Replace?.mockRestore(); mockNewVideoRepresentation3Replace?.mockRestore(); mockNewVideoRepresentation4Replace?.mockRestore(); mockNewAudioRepresentationReplace?.mockRestore(); mockNewVideoRepresentation1Update?.mockRestore(); mockNewVideoRepresentation2Update?.mockRestore(); mockNewVideoRepresentation3Update?.mockRestore(); mockNewVideoRepresentation4Update?.mockRestore(); mockNewAudioRepresentationUpdate?.mockRestore(); }); it("should fully update the first Period given by the second one in a full update", () => { const oldVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1, oldVideoRepresentation2], }); const oldVideoAdaptation2 = generateFakeAdaptation({ type: "video", id: "ada-video-2", representations: [oldVideoRepresentation3, oldVideoRepresentation4], }); const oldAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation], }); const oldPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [], adaptations: { video: [oldVideoAdaptation1, oldVideoAdaptation2], audio: [oldAudioAdaptation], }, getAdaptations() { return [oldVideoAdaptation1, oldVideoAdaptation2, oldAudioAdaptation]; }, }; const newVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [newVideoRepresentation1, newVideoRepresentation2], }); const newVideoAdaptation2 = generateFakeAdaptation({ type: "video", id: "ada-video-2", representations: [newVideoRepresentation3, newVideoRepresentation4], }); const newAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [newAudioRepresentation], }); const newPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [], adaptations: { video: [newVideoAdaptation1, newVideoAdaptation2], audio: [newAudioAdaptation], }, getAdaptations() { return [newVideoAdaptation1, newVideoAdaptation2, newAudioAdaptation]; }, }; const oldPeriodAdaptations = vi.spyOn(oldPeriod, "getAdaptations"); const newPeriodAdaptations = vi.spyOn(newPeriod, "getAdaptations"); const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Full, ); expect(res).toEqual({ addedThumbnailTracks: [], updatedThumbnailTracks: [], removedThumbnailTracks: [], addedAdaptations: [], removedAdaptations: [], updatedAdaptations: [ { adaptation: "ada-video-1", trackType: "video", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [ oldVideoRepresentation1.getMetadataSnapshot(), oldVideoRepresentation2.getMetadataSnapshot(), ], }, { adaptation: "ada-video-2", trackType: "video", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [ oldVideoRepresentation3.getMetadataSnapshot(), oldVideoRepresentation4.getMetadataSnapshot(), ], }, { adaptation: "ada-audio-1", trackType: "audio", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [oldAudioRepresentation.getMetadataSnapshot()], }, ], }); expect(oldPeriod.start).toEqual(500); expect(oldPeriod.end).toEqual(520); expect(oldPeriod.duration).toEqual(20); expect(oldPeriodAdaptations).toHaveBeenCalledTimes(1); expect(mockOldVideoRepresentation1Replace).toHaveBeenCalledTimes(1); expect(mockOldVideoRepresentation1Replace).toHaveBeenCalledWith( newVideoRepresentation1.index, ); expect(mockOldVideoRepresentation2Replace).toHaveBeenCalledTimes(1); expect(mockOldVideoRepresentation2Replace).toHaveBeenCalledWith( newVideoRepresentation2.index, ); expect(mockOldVideoRepresentation3Replace).toHaveBeenCalledTimes(1); expect(mockOldVideoRepresentation3Replace).toHaveBeenCalledWith( newVideoRepresentation3.index, ); expect(mockOldVideoRepresentation4Replace).toHaveBeenCalledTimes(1); expect(mockOldVideoRepresentation4Replace).toHaveBeenCalledWith( newVideoRepresentation4.index, ); expect(mockOldAudioRepresentationReplace).toHaveBeenCalledTimes(1); expect(mockOldAudioRepresentationReplace).toHaveBeenCalledWith( newAudioRepresentation.index, ); expect(newPeriod.start).toEqual(500); expect(newPeriod.end).toEqual(520); expect(newPeriod.duration).toEqual(20); expect(newPeriodAdaptations).toHaveBeenCalledTimes(1); expect(mockNewVideoRepresentation1Replace).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation2Replace).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation3Replace).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation4Replace).not.toHaveBeenCalled(); expect(mockNewAudioRepresentationReplace).not.toHaveBeenCalled(); expect(mockOldVideoRepresentation1Update).not.toHaveBeenCalled(); expect(mockOldVideoRepresentation2Update).not.toHaveBeenCalled(); expect(mockOldVideoRepresentation3Update).not.toHaveBeenCalled(); expect(mockOldVideoRepresentation4Update).not.toHaveBeenCalled(); expect(mockOldAudioRepresentationUpdate).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation1Update).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation2Update).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation3Update).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation4Update).not.toHaveBeenCalled(); expect(mockNewAudioRepresentationUpdate).not.toHaveBeenCalled(); expect(mockLog).toHaveBeenCalledTimes(0); mockLog.mockRestore(); }); it("should partially update the first Period given by the second one in a partial update", () => { const oldVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1, oldVideoRepresentation2], }); const oldVideoAdaptation2 = generateFakeAdaptation({ type: "video", id: "ada-video-2", representations: [oldVideoRepresentation3, oldVideoRepresentation4], }); const oldAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation], }); const oldPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [], adaptations: { video: [oldVideoAdaptation1, oldVideoAdaptation2], audio: [oldAudioAdaptation], }, getAdaptations() { return [oldVideoAdaptation1, oldVideoAdaptation2, oldAudioAdaptation]; }, }; const newVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [newVideoRepresentation1, newVideoRepresentation2], }); const newVideoAdaptation2 = generateFakeAdaptation({ type: "video", id: "ada-video-2", representations: [newVideoRepresentation3, newVideoRepresentation4], }); const newAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [newAudioRepresentation], }); const newPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [], adaptations: { video: [newVideoAdaptation1, newVideoAdaptation2], audio: [newAudioAdaptation], }, getAdaptations() { return [newVideoAdaptation1, newVideoAdaptation2, newAudioAdaptation]; }, }; const mockOldPeriodGetAdaptations = vi.spyOn(oldPeriod, "getAdaptations"); const mockNewPeriodGetAdaptations = vi.spyOn(newPeriod, "getAdaptations"); const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Partial, ); expect(res).toEqual({ addedThumbnailTracks: [], updatedThumbnailTracks: [], removedThumbnailTracks: [], addedAdaptations: [], removedAdaptations: [], updatedAdaptations: [ { adaptation: "ada-video-1", trackType: "video", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [ oldVideoRepresentation1.getMetadataSnapshot(), oldVideoRepresentation2.getMetadataSnapshot(), ], }, { adaptation: "ada-video-2", trackType: "video", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [ oldVideoRepresentation3.getMetadataSnapshot(), oldVideoRepresentation4.getMetadataSnapshot(), ], }, { adaptation: "ada-audio-1", trackType: "audio", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [oldAudioRepresentation.getMetadataSnapshot()], }, ], }); expect(oldPeriod.start).toEqual(500); expect(oldPeriod.end).toEqual(520); expect(oldPeriod.duration).toEqual(20); expect(mockOldPeriodGetAdaptations).toHaveBeenCalledTimes(1); expect(mockOldVideoRepresentation1Update).toHaveBeenCalledTimes(1); expect(mockOldVideoRepresentation1Update).toHaveBeenCalledWith( newVideoRepresentation1.index, ); expect(mockOldVideoRepresentation2Update).toHaveBeenCalledTimes(1); expect(mockOldVideoRepresentation2Update).toHaveBeenCalledWith( newVideoRepresentation2.index, ); expect(mockOldVideoRepresentation3Update).toHaveBeenCalledTimes(1); expect(mockOldVideoRepresentation3Update).toHaveBeenCalledWith( newVideoRepresentation3.index, ); expect(mockOldVideoRepresentation4Update).toHaveBeenCalledTimes(1); expect(mockOldVideoRepresentation4Update).toHaveBeenCalledWith( newVideoRepresentation4.index, ); expect(mockOldAudioRepresentationUpdate).toHaveBeenCalledTimes(1); expect(mockOldAudioRepresentationUpdate).toHaveBeenCalledWith( newAudioRepresentation.index, ); expect(newPeriod.start).toEqual(500); expect(newPeriod.end).toEqual(520); expect(newPeriod.duration).toEqual(20); expect(mockNewPeriodGetAdaptations).toHaveBeenCalledTimes(1); expect(mockNewVideoRepresentation1Update).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation2Update).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation3Update).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation4Update).not.toHaveBeenCalled(); expect(mockNewAudioRepresentationUpdate).not.toHaveBeenCalled(); expect(mockOldVideoRepresentation1Replace).not.toHaveBeenCalled(); expect(mockOldVideoRepresentation2Replace).not.toHaveBeenCalled(); expect(mockOldVideoRepresentation3Replace).not.toHaveBeenCalled(); expect(mockOldVideoRepresentation4Replace).not.toHaveBeenCalled(); expect(mockOldAudioRepresentationReplace).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation1Replace).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation2Replace).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation3Replace).not.toHaveBeenCalled(); expect(mockNewVideoRepresentation4Replace).not.toHaveBeenCalled(); expect(mockNewAudioRepresentationReplace).not.toHaveBeenCalled(); expect(mockLog).toHaveBeenCalledTimes(0); mockLog.mockRestore(); }); it("should add new Adaptations in Full mode", () => { const oldVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1, oldVideoRepresentation2], }); const oldAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation], }); const oldPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [], adaptations: { video: [oldVideoAdaptation1], audio: [oldAudioAdaptation], }, getAdaptations() { return [oldVideoAdaptation1, oldAudioAdaptation]; }, getMetadataSnapshot() { return {}; }, }; const newVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [newVideoRepresentation1, newVideoRepresentation2], }); const newVideoAdaptation2 = generateFakeAdaptation({ type: "video", id: "ada-video-2", representations: [newVideoRepresentation3, newVideoRepresentation4], }); const newAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [newAudioRepresentation], }); const newPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [], adaptations: { video: [newVideoAdaptation1, newVideoAdaptation2], audio: [newAudioAdaptation], }, getAdaptations() { return [newVideoAdaptation1, newVideoAdaptation2, newAudioAdaptation]; }, getMetadataSnapshot() { return {}; }, }; const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Full, ); expect(res).toEqual({ addedThumbnailTracks: [], updatedThumbnailTracks: [], removedThumbnailTracks: [], addedAdaptations: [newVideoAdaptation2.getMetadataSnapshot()], removedAdaptations: [], updatedAdaptations: [ { adaptation: "ada-video-1", trackType: "video", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [ oldVideoRepresentation1.getMetadataSnapshot(), oldVideoRepresentation2.getMetadataSnapshot(), ], }, { adaptation: "ada-audio-1", trackType: "audio", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [oldAudioRepresentation.getMetadataSnapshot()], }, ], }); expect(mockLog).toHaveBeenCalled(); expect(mockLog).toHaveBeenNthCalledWith( 1, "manifest", "1 new Adaptations found when merging.", ); expect(oldPeriod.adaptations.video).toHaveLength(2); mockLog.mockRestore(); }); it("should add new Adaptations in Partial mode", () => { const oldVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1, oldVideoRepresentation2], }); const oldAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation], }); const oldPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [], adaptations: { video: [oldVideoAdaptation1], audio: [oldAudioAdaptation], }, getAdaptations() { return [oldVideoAdaptation1, oldAudioAdaptation]; }, }; const newVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [newVideoRepresentation1, newVideoRepresentation2], }); const newVideoAdaptation2 = generateFakeAdaptation({ type: "video", id: "ada-video-2", representations: [newVideoRepresentation3, newVideoRepresentation4], }); const newAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [newAudioRepresentation], }); const newPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [], adaptations: { video: [newVideoAdaptation1, newVideoAdaptation2], audio: [newAudioAdaptation], }, getAdaptations() { return [newVideoAdaptation1, newVideoAdaptation2, newAudioAdaptation]; }, getMetadataSnapshot() { return {}; }, }; const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Partial, ); expect(res).toEqual({ addedThumbnailTracks: [], updatedThumbnailTracks: [], removedThumbnailTracks: [], addedAdaptations: [newVideoAdaptation2.getMetadataSnapshot()], removedAdaptations: [], updatedAdaptations: [ { adaptation: "ada-video-1", trackType: "video", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [ oldVideoRepresentation1.getMetadataSnapshot(), oldVideoRepresentation2.getMetadataSnapshot(), ], }, { adaptation: "ada-audio-1", trackType: "audio", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [oldAudioRepresentation.getMetadataSnapshot()], }, ], }); expect(mockLog).toHaveBeenCalled(); expect(mockLog).toHaveBeenNthCalledWith( 1, "manifest", "1 new Adaptations found when merging.", ); expect(oldPeriod.adaptations.video).toHaveLength(2); mockLog.mockRestore(); }); it("should remove unfound Adaptations in Full mode", () => { const oldVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1, oldVideoRepresentation2], }); const oldVideoAdaptation2 = generateFakeAdaptation({ type: "video", id: "ada-video-2", representations: [oldVideoRepresentation3, oldVideoRepresentation4], }); const oldAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation], }); const oldPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [], adaptations: { video: [oldVideoAdaptation1, oldVideoAdaptation2], audio: [oldAudioAdaptation], }, getAdaptations() { return [oldVideoAdaptation1, oldVideoAdaptation2, oldAudioAdaptation]; }, }; const newVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [newVideoRepresentation1, newVideoRepresentation2], }); const newAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [newAudioRepresentation], }); const newPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [], adaptations: { video: [newVideoAdaptation1], audio: [newAudioAdaptation], }, getAdaptations() { return [newVideoAdaptation1, newAudioAdaptation]; }, getMetadataSnapshot() { return {}; }, }; const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Full, ); expect(res).toEqual({ addedThumbnailTracks: [], updatedThumbnailTracks: [], removedThumbnailTracks: [], addedAdaptations: [], removedAdaptations: [ { id: "ada-video-2", trackType: "video", }, ], updatedAdaptations: [ { adaptation: "ada-video-1", trackType: "video", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [ oldVideoRepresentation1.getMetadataSnapshot(), oldVideoRepresentation2.getMetadataSnapshot(), ], }, { adaptation: "ada-audio-1", trackType: "audio", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [oldAudioRepresentation.getMetadataSnapshot()], }, ], }); expect(mockLog).toHaveBeenCalled(); expect(mockLog).toHaveBeenNthCalledWith( 1, "manifest", 'Adaptation "ada-video-2" not found when merging.', ); expect(oldPeriod.adaptations.video).toHaveLength(2); mockLog.mockRestore(); }); it("should remove unfound Adaptations in Partial mode", () => { const oldVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1, oldVideoRepresentation2], }); const oldVideoAdaptation2 = generateFakeAdaptation({ type: "video", id: "ada-video-2", representations: [oldVideoRepresentation3, oldVideoRepresentation4], }); const oldAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation], }); const oldPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [], adaptations: { video: [oldVideoAdaptation1, oldVideoAdaptation2], audio: [oldAudioAdaptation], }, getAdaptations() { return [oldVideoAdaptation1, oldVideoAdaptation2, oldAudioAdaptation]; }, }; const newVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [newVideoRepresentation1, newVideoRepresentation2], }); const newAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [newAudioRepresentation], }); const newPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [], adaptations: { video: [newVideoAdaptation1], audio: [newAudioAdaptation], }, getAdaptations() { return [newVideoAdaptation1, newAudioAdaptation]; }, getMetadataSnapshot() { return {}; }, }; const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Partial, ); expect(res).toEqual({ addedThumbnailTracks: [], updatedThumbnailTracks: [], removedThumbnailTracks: [], addedAdaptations: [], removedAdaptations: [ { id: "ada-video-2", trackType: "video", }, ], updatedAdaptations: [ { adaptation: "ada-video-1", trackType: "video", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [ oldVideoRepresentation1.getMetadataSnapshot(), oldVideoRepresentation2.getMetadataSnapshot(), ], }, { adaptation: "ada-audio-1", trackType: "audio", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [oldAudioRepresentation.getMetadataSnapshot()], }, ], }); expect(mockLog).toHaveBeenCalled(); expect(mockLog).toHaveBeenNthCalledWith( 1, "manifest", 'Adaptation "ada-video-2" not found when merging.', ); expect(oldPeriod.adaptations.video).toHaveLength(2); mockLog.mockRestore(); }); it("should add new Representations in Full mode", () => { const oldVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1], }); const oldAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation], }); const oldPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [], adaptations: { video: [oldVideoAdaptation1], audio: [oldAudioAdaptation], }, getAdaptations() { return [oldVideoAdaptation1, oldAudioAdaptation]; }, }; const newVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [newVideoRepresentation1, newVideoRepresentation2], }); const newAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [newAudioRepresentation], }); const newPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [], adaptations: { video: [newVideoAdaptation1], audio: [newAudioAdaptation], }, getAdaptations() { return [newVideoAdaptation1, newAudioAdaptation]; }, getMetadataSnapshot() { return {}; }, }; const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Full, ); expect(res).toEqual({ addedThumbnailTracks: [], updatedThumbnailTracks: [], removedThumbnailTracks: [], addedAdaptations: [], removedAdaptations: [], updatedAdaptations: [ { adaptation: "ada-video-1", trackType: "video", addedRepresentations: [newVideoRepresentation2.getMetadataSnapshot()], removedRepresentations: [], updatedRepresentations: [oldVideoRepresentation1.getMetadataSnapshot()], }, { adaptation: "ada-audio-1", trackType: "audio", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [oldAudioRepresentation.getMetadataSnapshot()], }, ], }); expect(mockLog).toHaveBeenCalledTimes(1); expect(mockLog).toHaveBeenCalledWith( "manifest", "1 new Representations found when merging.", ); expect(oldVideoAdaptation1.representations).toHaveLength(2); mockLog.mockRestore(); }); it("should add new Representations in Partial mode", () => { const oldVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1], }); const oldAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation], }); const oldPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [], adaptations: { video: [oldVideoAdaptation1], audio: [oldAudioAdaptation], }, getAdaptations() { return [oldVideoAdaptation1, oldAudioAdaptation]; }, }; const newVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [newVideoRepresentation1, newVideoRepresentation2], }); const newAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [newAudioRepresentation], }); const newPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [], adaptations: { video: [newVideoAdaptation1], audio: [newAudioAdaptation], }, getAdaptations() { return [newVideoAdaptation1, newAudioAdaptation]; }, getMetadataSnapshot() { return {}; }, }; const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Partial, ); expect(res).toEqual({ addedThumbnailTracks: [], updatedThumbnailTracks: [], removedThumbnailTracks: [], addedAdaptations: [], removedAdaptations: [], updatedAdaptations: [ { adaptation: "ada-video-1", trackType: "video", addedRepresentations: [newVideoRepresentation2.getMetadataSnapshot()], removedRepresentations: [], updatedRepresentations: [oldVideoRepresentation1.getMetadataSnapshot()], }, { adaptation: "ada-audio-1", trackType: "audio", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [oldAudioRepresentation.getMetadataSnapshot()], }, ], }); expect(mockLog).toHaveBeenCalledTimes(1); expect(mockLog).toHaveBeenCalledWith( "manifest", "1 new Representations found when merging.", ); expect(oldVideoAdaptation1.representations).toHaveLength(2); mockLog.mockRestore(); }); it("should remove an old Representation that is not found in Full mode", () => { const oldVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1, oldVideoRepresentation2], }); const oldAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation], }); const oldPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [], adaptations: { video: [oldVideoAdaptation1], audio: [oldAudioAdaptation], }, getAdaptations() { return [oldVideoAdaptation1, oldAudioAdaptation]; }, }; const newVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [newVideoRepresentation1], }); const newAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [newAudioRepresentation], }); const newPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [], adaptations: { video: [newVideoAdaptation1], audio: [newAudioAdaptation], }, getAdaptations() { return [newVideoAdaptation1, newAudioAdaptation]; }, getMetadataSnapshot() { return {}; }, }; const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Full, ); expect(res).toEqual({ addedThumbnailTracks: [], updatedThumbnailTracks: [], removedThumbnailTracks: [], addedAdaptations: [], removedAdaptations: [], updatedAdaptations: [ { adaptation: "ada-video-1", trackType: "video", addedRepresentations: [], removedRepresentations: [oldVideoRepresentation2.id], updatedRepresentations: [oldVideoRepresentation1.getMetadataSnapshot()], }, { adaptation: "ada-audio-1", trackType: "audio", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [oldAudioRepresentation.getMetadataSnapshot()], }, ], }); expect(mockLog).toHaveBeenCalledTimes(1); expect(mockLog).toHaveBeenCalledWith( "manifest", 'Representation "rep-video-2" not found when merging.', ); expect(oldVideoAdaptation1.representations).toHaveLength(1); mockLog.mockRestore(); }); it("should remove an old Representation that is not found in Partial mode", () => { const oldVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1, oldVideoRepresentation2], }); const oldAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation], }); const oldPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [], adaptations: { video: [oldVideoAdaptation1], audio: [oldAudioAdaptation], }, getAdaptations() { return [oldVideoAdaptation1, oldAudioAdaptation]; }, }; const newVideoAdaptation1 = generateFakeAdaptation({ type: "video", id: "ada-video-1", representations: [newVideoRepresentation1], }); const newAudioAdaptation = generateFakeAdaptation({ type: "audio", id: "ada-audio-1", representations: [newAudioRepresentation], }); const newPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [], adaptations: { video: [newVideoAdaptation1], audio: [newAudioAdaptation], }, getAdaptations() { return [newVideoAdaptation1, newAudioAdaptation]; }, getMetadataSnapshot() { return {}; }, }; const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Partial, ); expect(res).toEqual({ addedThumbnailTracks: [], updatedThumbnailTracks: [], removedThumbnailTracks: [], addedAdaptations: [], removedAdaptations: [], updatedAdaptations: [ { adaptation: "ada-video-1", trackType: "video", addedRepresentations: [], removedRepresentations: [oldVideoRepresentation2.id], updatedRepresentations: [oldVideoRepresentation1.getMetadataSnapshot()], }, { adaptation: "ada-audio-1", trackType: "audio", addedRepresentations: [], removedRepresentations: [], updatedRepresentations: [oldAudioRepresentation.getMetadataSnapshot()], }, ], }); expect(mockLog).toHaveBeenCalledTimes(1); expect(mockLog).toHaveBeenCalledWith( "manifest", 'Representation "rep-video-2" not found when merging.', ); expect(oldVideoAdaptation1.representations).toHaveLength(1); mockLog.mockRestore(); }); it("should add new Thumbnail Track in Full mode", () => { const oldThumbnailTrack1 = generateFakeThumbnailTrack({ id: "thumb-1", }); const oldPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [oldThumbnailTrack1], adaptations: { video: [], audio: [], }, getAdaptations() { return []; }, }; const newThumbnailTrack1 = generateFakeThumbnailTrack({ id: "thumb-1", }); const newThumbnailTrack2 = generateFakeThumbnailTrack({ id: "thumb-2", }); const newPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [newThumbnailTrack1, newThumbnailTrack2], adaptations: { video: [], audio: [], }, getAdaptations() { return []; }, }; const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Full, ); expect(res).toEqual({ addedThumbnailTracks: [ { height: 100, horizontalTiles: 5, id: "thumb-2", mimeType: "image/png", verticalTiles: 3, start: 0, end: 100, tileDuration: 2, width: 200, }, ], updatedThumbnailTracks: [ { height: 100, horizontalTiles: 5, id: "thumb-1", mimeType: "image/png", verticalTiles: 3, start: 0, end: 100, tileDuration: 2, width: 200, }, ], removedThumbnailTracks: [], addedAdaptations: [], removedAdaptations: [], updatedAdaptations: [], }); expect(mockLog).toHaveBeenCalled(); expect(mockLog).toHaveBeenNthCalledWith( 1, "manifest", "1 new Thumbnail tracks found when merging.", ); expect(oldPeriod.thumbnailTracks).toHaveLength(2); mockLog.mockRestore(); }); it("should add new Thumbnail Track in Partial mode", () => { const oldThumbnailTrack1 = generateFakeThumbnailTrack({ id: "thumb-1", }); const oldPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [oldThumbnailTrack1], adaptations: { video: [], audio: [], }, getAdaptations() { return []; }, }; const newThumbnailTrack1 = generateFakeThumbnailTrack({ id: "thumb-1", }); const newThumbnailTrack2 = generateFakeThumbnailTrack({ id: "thumb-2", }); const newPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [newThumbnailTrack1, newThumbnailTrack2], adaptations: { video: [], audio: [], }, getAdaptations() { return []; }, }; const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Partial, ); expect(res).toEqual({ addedThumbnailTracks: [ { height: 100, horizontalTiles: 5, id: "thumb-2", mimeType: "image/png", verticalTiles: 3, start: 0, end: 100, tileDuration: 2, width: 200, }, ], updatedThumbnailTracks: [ { height: 100, horizontalTiles: 5, id: "thumb-1", mimeType: "image/png", verticalTiles: 3, start: 0, end: 100, tileDuration: 2, width: 200, }, ], removedThumbnailTracks: [], addedAdaptations: [], removedAdaptations: [], updatedAdaptations: [], }); expect(mockLog).toHaveBeenCalled(); expect(mockLog).toHaveBeenNthCalledWith( 1, "manifest", "1 new Thumbnail tracks found when merging.", ); expect(oldPeriod.thumbnailTracks).toHaveLength(2); mockLog.mockRestore(); }); it("should remove unfound Thumbnail Tracks in Full mode", () => { const oldThumbnailTrack1 = generateFakeThumbnailTrack({ id: "thumb-1", }); const oldThumbnailTrack2 = generateFakeThumbnailTrack({ id: "thumb-2", }); const oldPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [oldThumbnailTrack1, oldThumbnailTrack2], adaptations: { video: [], audio: [], }, getAdaptations() { return []; }, }; const newThumbnailTrack1 = generateFakeThumbnailTrack({ id: "thumb-1", }); const newPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [newThumbnailTrack1], adaptations: { video: [], audio: [], }, getAdaptations() { return []; }, }; const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Full, ); expect(res).toEqual({ addedThumbnailTracks: [], updatedThumbnailTracks: [ { height: 100, horizontalTiles: 5, id: "thumb-1", mimeType: "image/png", verticalTiles: 3, start: 0, end: 100, tileDuration: 2, width: 200, }, ], removedThumbnailTracks: [ { id: oldThumbnailTrack2.id, }, ], addedAdaptations: [], removedAdaptations: [], updatedAdaptations: [], }); expect(mockLog).toHaveBeenCalled(); expect(mockLog).toHaveBeenNthCalledWith( 1, "manifest", 'ThumbnailTrack "thumb-2" not found when merging.', ); expect(oldPeriod.thumbnailTracks).toHaveLength(1); mockLog.mockRestore(); }); it("should remove unfound ThumbnailTracks in Partial mode", () => { const oldThumbnailTrack1 = generateFakeThumbnailTrack({ id: "thumb-1", }); const oldThumbnailTrack2 = generateFakeThumbnailTrack({ id: "thumb-2", }); const oldPeriod = { start: 5, end: 15, duration: 10, thumbnailTracks: [oldThumbnailTrack1, oldThumbnailTrack2], adaptations: { video: [], audio: [], }, getAdaptations() { return []; }, }; const newThumbnailTrack1 = generateFakeThumbnailTrack({ id: "thumb-1", }); const newPeriod = { start: 500, end: 520, duration: 20, thumbnailTracks: [newThumbnailTrack1], adaptations: { video: [], audio: [], }, getAdaptations() { return []; }, }; const mockLog = vi.spyOn(log, "warn"); const res = updatePeriodInPlace( oldPeriod as unknown as Period, newPeriod as unknown as Period, MANIFEST_UPDATE_TYPE.Partial, ); expect(res).toEqual({ addedThumbnailTracks: [], updatedThumbnailTracks: [ { height: 100, horizontalTiles: 5, id: "thumb-1", mimeType: "image/png", verticalTiles: 3, start: 0, end: 100, tileDuration: 2, width: 200, }, ], removedThumbnailTracks: [ { id: oldThumbnailTrack2.id, }, ], addedAdaptations: [], removedAdaptations: [], updatedAdaptations: [], }); expect(mockLog).toHaveBeenCalled(); expect(mockLog).toHaveBeenNthCalledWith( 1, "manifest", 'ThumbnailTrack "thumb-2" not found when merging.', ); expect(oldPeriod.thumbnailTracks).toHaveLength(1); mockLog.mockRestore(); }); });