diff --git a/package-lock.json b/package-lock.json index 9d684b8315c..d359381202e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,7 +80,7 @@ "sinon-chai": "4.0.1", "typescript": "5.9.3", "url-toolkit": "2.2.5", - "wrangler": "4.147.0" + "wrangler": "4.148.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1850,9 +1850,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20261001.1.tgz", - "integrity": "sha512-4cgSgDf28JSw/P5Dj5GCS59hzVqS5XnmGAWNkvYHLI6ODU9idGaMMNEuhJXDkEG/lsABmlVlnCgsdh2VbKWepw==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20261006.1.tgz", + "integrity": "sha512-lCX6avJO2It9AyDEYitjmOtJRVp/xmaW0TziyoLAnfxlzozyyRbSFfIIMMCiDx/sTP1++E4VLbV+7BHSD33f0Q==", "cpu": [ "x64" ], @@ -1867,9 +1867,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20261001.1.tgz", - "integrity": "sha512-8ulAWruEVouNmEIsQsy9WSSCS9zkLu93W2MTwp5esiFyoPp05BNSVFIFsYSPi1pkFmBBd7fsnwMpbLkaJLaVPQ==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20261006.1.tgz", + "integrity": "sha512-PyFGjOaldtppnLuKsdkoYWXfLVpDjuf8u+uoykX+1Xvxo2EPmCVViq0iDKm+U7c7PRno85ehS3/MPVXyk2HwHw==", "cpu": [ "arm64" ], @@ -1884,9 +1884,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20261001.1.tgz", - "integrity": "sha512-kZbTZJGrhsMOdqZ2BIybjaBRLZjYsRLWj7mcNw/6Y3hodOhp5MrNzcz4iWGwNVWwyIV+7Pl+/LX5VcgnRqdHOg==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20261006.1.tgz", + "integrity": "sha512-Ntf9S8eesmVufdjaT2kzWlz6Ng9XGYTc6cHQVRKAg59lFufwsIabGeMT6U4KkrpEeSIxgHq8aPtuLbZXE19tCA==", "cpu": [ "x64" ], @@ -1901,9 +1901,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20261001.1.tgz", - "integrity": "sha512-oOk3Zj6k/8oP0FJgZBWDn7+BqbsqIMEMaV95pulHPVbwVu4wYoLfQq2hp0vkbCNsCFLqZb7cqixXdrwD54ZIow==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20261006.1.tgz", + "integrity": "sha512-JyeDBymVWw5tVRebSsVE8ojDYxd6UlZrAseHU8G+NpIyd5wF61tXlF/axLeCUqu0pOhBFHb8I5QKi0jwrCK41Q==", "cpu": [ "arm64" ], @@ -1918,9 +1918,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20261001.1.tgz", - "integrity": "sha512-uRxm5W4VyBkoSaoP1BfOuH0873tE+vxsknF4sab6/5YIRvIAufChq3QDp+zlAn67PuGPGcn7W8QraA0t4ucmOQ==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20261006.1.tgz", + "integrity": "sha512-nWP0URNqOBN9fa/gZ7eh/w051DwjOXN8j4lDZDYVXyQPsux6sV3DfyYZT+CjR/rt9nKBSmJT9QE9/bWT18+84w==", "cpu": [ "x64" ], @@ -10229,16 +10229,16 @@ } }, "node_modules/miniflare": { - "version": "5.20261001.0-alpha", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20261001.0-alpha.tgz", - "integrity": "sha512-GaimS5mSIOMyvd16ga+e1/QkI8cmp3z35QPHFex0DktqNQf2RVZQwMPQXdyoUreUiFP/0Gpe2MoQInTyMAD5xA==", + "version": "5.20261006.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20261006.0-alpha.tgz", + "integrity": "sha512-UP3qIeQKHba59GHxBqD8H2GateacluT4vrjIpqEgH78kCSUxdu8jA0f7MH7ftRSAnnB1oOvEsDtav3xevQDbpQ==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.4", "undici": "7.29.1", - "workerd": "1.20261001.1", + "workerd": "1.20261006.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, @@ -13948,9 +13948,9 @@ "dev": true }, "node_modules/workerd": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20261001.1.tgz", - "integrity": "sha512-d/SIYHFO0PT/wiFZg8in4NpRIxYuFwslX1HdylOtWkBIIUmSpkGFhK820cV84XACFylwJ48xuRoWW/8DWDPsPQ==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20261006.1.tgz", + "integrity": "sha512-8MadBuffZ5NdpLwZjUmsqT5QlaKXq4gjgT6wzcKeynZTICNXyePhc+5QVoKiK7+MMhFTLNrRO2KXLlpk8Q6BkA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -13961,11 +13961,11 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20261001.1", - "@cloudflare/workerd-darwin-arm64": "1.20261001.1", - "@cloudflare/workerd-linux-64": "1.20261001.1", - "@cloudflare/workerd-linux-arm64": "1.20261001.1", - "@cloudflare/workerd-windows-64": "1.20261001.1" + "@cloudflare/workerd-darwin-64": "1.20261006.1", + "@cloudflare/workerd-darwin-arm64": "1.20261006.1", + "@cloudflare/workerd-linux-64": "1.20261006.1", + "@cloudflare/workerd-linux-arm64": "1.20261006.1", + "@cloudflare/workerd-windows-64": "1.20261006.1" } }, "node_modules/workerpool": { @@ -13975,9 +13975,9 @@ "dev": true }, "node_modules/wrangler": { - "version": "4.147.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.147.0.tgz", - "integrity": "sha512-pQYRoiq8PTAxphaG69z8+GC1DkSGd19EDZehQ8zxjo/Ko3mRB6Qs1mTrd8ZuKAarLklIjTqr1lUdCK9r4q2hUg==", + "version": "4.148.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.148.0.tgz", + "integrity": "sha512-wgbll8cA/7qOMJSoYQuJrw9M9lmedGzYtg6wvUK2p/7G1KaE88jFmQ/CvtQzVUF9C8PQtC4Y5p9vbpAsJGAlpA==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { @@ -13985,10 +13985,10 @@ "@cloudflare/unenv-preset": "2.16.2", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "5.20261001.0-alpha", + "miniflare": "5.20261006.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20261001.1" + "workerd": "1.20261006.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -14002,7 +14002,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^5.20261001.1" + "@cloudflare/workers-types": "^5.20261006.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -15526,37 +15526,37 @@ "requires": {} }, "@cloudflare/workerd-darwin-64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20261001.1.tgz", - "integrity": "sha512-4cgSgDf28JSw/P5Dj5GCS59hzVqS5XnmGAWNkvYHLI6ODU9idGaMMNEuhJXDkEG/lsABmlVlnCgsdh2VbKWepw==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20261006.1.tgz", + "integrity": "sha512-lCX6avJO2It9AyDEYitjmOtJRVp/xmaW0TziyoLAnfxlzozyyRbSFfIIMMCiDx/sTP1++E4VLbV+7BHSD33f0Q==", "dev": true, "optional": true }, "@cloudflare/workerd-darwin-arm64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20261001.1.tgz", - "integrity": "sha512-8ulAWruEVouNmEIsQsy9WSSCS9zkLu93W2MTwp5esiFyoPp05BNSVFIFsYSPi1pkFmBBd7fsnwMpbLkaJLaVPQ==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20261006.1.tgz", + "integrity": "sha512-PyFGjOaldtppnLuKsdkoYWXfLVpDjuf8u+uoykX+1Xvxo2EPmCVViq0iDKm+U7c7PRno85ehS3/MPVXyk2HwHw==", "dev": true, "optional": true }, "@cloudflare/workerd-linux-64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20261001.1.tgz", - "integrity": "sha512-kZbTZJGrhsMOdqZ2BIybjaBRLZjYsRLWj7mcNw/6Y3hodOhp5MrNzcz4iWGwNVWwyIV+7Pl+/LX5VcgnRqdHOg==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20261006.1.tgz", + "integrity": "sha512-Ntf9S8eesmVufdjaT2kzWlz6Ng9XGYTc6cHQVRKAg59lFufwsIabGeMT6U4KkrpEeSIxgHq8aPtuLbZXE19tCA==", "dev": true, "optional": true }, "@cloudflare/workerd-linux-arm64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20261001.1.tgz", - "integrity": "sha512-oOk3Zj6k/8oP0FJgZBWDn7+BqbsqIMEMaV95pulHPVbwVu4wYoLfQq2hp0vkbCNsCFLqZb7cqixXdrwD54ZIow==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20261006.1.tgz", + "integrity": "sha512-JyeDBymVWw5tVRebSsVE8ojDYxd6UlZrAseHU8G+NpIyd5wF61tXlF/axLeCUqu0pOhBFHb8I5QKi0jwrCK41Q==", "dev": true, "optional": true }, "@cloudflare/workerd-windows-64": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20261001.1.tgz", - "integrity": "sha512-uRxm5W4VyBkoSaoP1BfOuH0873tE+vxsknF4sab6/5YIRvIAufChq3QDp+zlAn67PuGPGcn7W8QraA0t4ucmOQ==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20261006.1.tgz", + "integrity": "sha512-nWP0URNqOBN9fa/gZ7eh/w051DwjOXN8j4lDZDYVXyQPsux6sV3DfyYZT+CjR/rt9nKBSmJT9QE9/bWT18+84w==", "dev": true, "optional": true }, @@ -21074,15 +21074,15 @@ "dev": true }, "miniflare": { - "version": "5.20261001.0-alpha", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20261001.0-alpha.tgz", - "integrity": "sha512-GaimS5mSIOMyvd16ga+e1/QkI8cmp3z35QPHFex0DktqNQf2RVZQwMPQXdyoUreUiFP/0Gpe2MoQInTyMAD5xA==", + "version": "5.20261006.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20261006.0-alpha.tgz", + "integrity": "sha512-UP3qIeQKHba59GHxBqD8H2GateacluT4vrjIpqEgH78kCSUxdu8jA0f7MH7ftRSAnnB1oOvEsDtav3xevQDbpQ==", "dev": true, "requires": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.4", "undici": "7.29.1", - "workerd": "1.20261001.1", + "workerd": "1.20261006.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } @@ -23751,16 +23751,16 @@ "dev": true }, "workerd": { - "version": "1.20261001.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20261001.1.tgz", - "integrity": "sha512-d/SIYHFO0PT/wiFZg8in4NpRIxYuFwslX1HdylOtWkBIIUmSpkGFhK820cV84XACFylwJ48xuRoWW/8DWDPsPQ==", + "version": "1.20261006.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20261006.1.tgz", + "integrity": "sha512-8MadBuffZ5NdpLwZjUmsqT5QlaKXq4gjgT6wzcKeynZTICNXyePhc+5QVoKiK7+MMhFTLNrRO2KXLlpk8Q6BkA==", "dev": true, "requires": { - "@cloudflare/workerd-darwin-64": "1.20261001.1", - "@cloudflare/workerd-darwin-arm64": "1.20261001.1", - "@cloudflare/workerd-linux-64": "1.20261001.1", - "@cloudflare/workerd-linux-arm64": "1.20261001.1", - "@cloudflare/workerd-windows-64": "1.20261001.1" + "@cloudflare/workerd-darwin-64": "1.20261006.1", + "@cloudflare/workerd-darwin-arm64": "1.20261006.1", + "@cloudflare/workerd-linux-64": "1.20261006.1", + "@cloudflare/workerd-linux-arm64": "1.20261006.1", + "@cloudflare/workerd-windows-64": "1.20261006.1" } }, "workerpool": { @@ -23770,9 +23770,9 @@ "dev": true }, "wrangler": { - "version": "4.147.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.147.0.tgz", - "integrity": "sha512-pQYRoiq8PTAxphaG69z8+GC1DkSGd19EDZehQ8zxjo/Ko3mRB6Qs1mTrd8ZuKAarLklIjTqr1lUdCK9r4q2hUg==", + "version": "4.148.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.148.0.tgz", + "integrity": "sha512-wgbll8cA/7qOMJSoYQuJrw9M9lmedGzYtg6wvUK2p/7G1KaE88jFmQ/CvtQzVUF9C8PQtC4Y5p9vbpAsJGAlpA==", "dev": true, "requires": { "@cloudflare/kv-asset-handler": "0.5.0", @@ -23780,10 +23780,10 @@ "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "fsevents": "2.3.3", - "miniflare": "5.20261001.0-alpha", + "miniflare": "5.20261006.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20261001.1" + "workerd": "1.20261006.1" } }, "wrap-ansi": { diff --git a/package.json b/package.json index 9daca509762..594dec2e625 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,6 @@ "sinon-chai": "4.0.1", "typescript": "5.9.3", "url-toolkit": "2.2.5", - "wrangler": "4.147.0" + "wrangler": "4.148.0" } } diff --git a/src/remux/mp4-remuxer.ts b/src/remux/mp4-remuxer.ts index 96b0e59b8c0..b91d86afecd 100644 --- a/src/remux/mp4-remuxer.ts +++ b/src/remux/mp4-remuxer.ts @@ -486,7 +486,9 @@ export default class MP4Remuxer extends Logger implements Remuxer { ) { initDTS = Math.min(initDTS as number, videoInitDTS); // Bind initPTS to the lowest DTS across tracks to prevent negative timestamps - initPTS = Math.min(initPTS as number, videoInitDTS); + const videoInit = + !_initPTS && this.isVideoContiguous ? videoInitPTS : videoInitDTS; + initPTS = Math.min(initPTS as number, videoInit); } } this.videoTrackConfig = { diff --git a/tests/unit/remux/mp4-remuxer.ts b/tests/unit/remux/mp4-remuxer.ts index 8466fe2e2a4..c763f325390 100644 --- a/tests/unit/remux/mp4-remuxer.ts +++ b/tests/unit/remux/mp4-remuxer.ts @@ -2,14 +2,27 @@ import { expect, use } from 'chai'; import EventEmitter from 'eventemitter3'; import sinonChai from 'sinon-chai'; import { hlsDefaultConfig } from '../../../src/config'; +import { dummyTrack } from '../../../src/demux/dummy-demuxed-track'; import MP4Remuxer from '../../../src/remux/mp4-remuxer'; +import { PlaylistLevelType } from '../../../src/types/loader'; +import { ChunkMetadata } from '../../../src/types/transmuxer'; import { logger } from '../../../src/utils/logger'; import type { HlsEventEmitter } from '../../../src/events'; -import type { VideoSample } from '../../../src/types/demuxer'; +import type { + AudioSample, + DemuxedAudioTrack, + DemuxedMetadataTrack, + DemuxedUserdataTrack, + DemuxedVideoTrack, + VideoSample, +} from '../../../src/types/demuxer'; import type { TypeSupported } from '../../../src/utils/codecs'; use(sinonChai); +const VIDEO_FRAME_DURATION = 3600; // 25 fps in 90 kHz ticks +const AAC_FRAME_DURATION = 1920; // 1024 samples at 48 kHz in 90 kHz ticks + describe('mp4-remuxer', function () { let mp4Remuxer: MP4Remuxer; @@ -102,6 +115,118 @@ describe('mp4-remuxer', function () { ); expect(result).to.be.gt(0); }); + + describe('initPTS anchoring', function () { + const frames = 100; // 4 seconds at 25 fps + const fragmentDuration = (frames * VIDEO_FRAME_DURATION) / 90000; + + it('anchors the first segment of a session on video DTS so earlier segments keep composition headroom', function () { + // start mid-stream at playlist time 20 (#7932): the first DTS maps to 20, the first PTS to 20 + cts + const firstDts = 1800000 + 7830; + const result = remuxFragment( + mp4Remuxer, + 3, + makeVideoTrack(firstDts, 7200, frames), + makeAudioTrack(firstDts + 9200, 185), + 20, + ); + expect(result.initSegment?.initPTS).to.equal(firstDts - 20 * 90000); + expect(result.video?.startPTS).to.be.closeTo(20 + 7200 / 90000, 0.001); + }); + + it('anchors on video DTS after seeking (non-contiguous) into another discontinuity sequence', function () { + remuxFragment( + mp4Remuxer, + 3, + makeVideoTrack(1800000, 7200, frames), + makeAudioTrack(1800000 + 9200, 185), + 20, + ); + // seek: the transmuxer resets the init segment, the timestamp offset (new cc, no cached initPTS) and contiguity + mp4Remuxer.resetInitSegment(); + mp4Remuxer.resetTimeStamp(null); + mp4Remuxer.resetNextTimestamp(); + const firstDts = 30000000; + const result = remuxFragment( + mp4Remuxer, + 40, + makeVideoTrack(firstDts, 7200, frames), + makeAudioTrack(firstDts + 9200, 185), + 300, + ); + expect(result.initSegment?.initPTS).to.equal(firstDts - 300 * 90000); + }); + + it('does not shift the timeline by the composition offset at each discontinuity (#8064)', function () { + // cc 0 mirrors the first segment of the deltatre DAI test stream: cts 40 ms, first audio 99 ms after the first video DTS + let result = remuxFragment( + mp4Remuxer, + 0, + makeVideoTrack(900000, 3600, frames), + makeAudioTrack(900000 + 8951, 185), + 0, + ); + let timeOffset = Math.max(result.video!.endPTS, result.audio!.endPTS); + // cc 1 to 3 mirror the ad segments: cts 80 ms, first audio 101 ms then 80 ms after the first video DTS + const sequences = [ + { dts: 118800, cts: 7200, audioOffset: 9119 }, + { dts: 118800, cts: 7200, audioOffset: 7200 }, + { dts: 118800, cts: 7200, audioOffset: 7200 }, + ]; + sequences.forEach(({ dts, cts, audioOffset }, i) => { + // contiguous discontinuity: the transmuxer resets the init segment and the timestamp offset, but not contiguity + mp4Remuxer.resetInitSegment(); + mp4Remuxer.resetTimeStamp(null); + result = remuxFragment( + mp4Remuxer, + i + 1, + makeVideoTrack(dts, cts, frames), + makeAudioTrack(dts + audioOffset, 185), + timeOffset, + ); + // the remuxed video must end one fragment duration after the timeOffset it was asked to start at + expect(result.video!.endPTS, `cc ${i + 1} video endPTS`).to.be.closeTo( + timeOffset + fragmentDuration, + 0.001, + ); + timeOffset = Math.max(result.video!.endPTS, result.audio!.endPTS); + }); + }); + + it('anchors on video DTS at a discontinuity reached with a level switch (not contiguous)', function () { + let result = remuxFragment( + mp4Remuxer, + 0, + makeVideoTrack(900000, 3600, frames), + makeAudioTrack(900000 + 8951, 185), + 0, + ); + const timeOffset = Math.max(result.video!.endPTS, result.audio!.endPTS); + // level switch at the discontinuity: the transmuxer resets the init segment, the timestamp + // offset and contiguity + mp4Remuxer.resetInitSegment(); + mp4Remuxer.resetTimeStamp(null); + mp4Remuxer.resetNextTimestamp(); + // timestamps above the playlist time, so computeInitPts needs no 33-bit rollover adjustment + const dts = 9000000; + const cts = 7200; + result = remuxFragment( + mp4Remuxer, + 1, + makeVideoTrack(dts, cts, frames), + makeAudioTrack(dts + 9119, 185), + timeOffset, + ); + // the DTS anchor keeps its headroom across the switch, at the cost of one composition offset + expect(result.initSegment?.initPTS).to.equal( + dts - Math.round(timeOffset * 90000), + ); + expect(result.video!.endPTS).to.be.closeTo( + timeOffset + fragmentDuration + cts / 90000, + 0.001, + ); + }); + }); }); function ptsDts(pts: number, dts: number): VideoSample { @@ -119,3 +244,81 @@ function ptsDts(pts: number, dts: number): VideoSample { length: 1, }; } + +function makeVideoTrack( + firstDts: number, + cts: number, + frames: number, +): DemuxedVideoTrack { + const samples: VideoSample[] = []; + for (let i = 0; i < frames; i++) { + const dts = firstDts + i * VIDEO_FRAME_DURATION; + samples.push({ + dts, + pts: dts + cts, + key: i === 0, + frame: true, + units: [{ data: new Uint8Array(8), type: i === 0 ? 5 : 1 }], + length: 8, + }); + } + return { + ...dummyTrack('video'), + type: 'video', + id: 1, + pid: 256, + segmentCodec: 'avc', + codec: 'avc1.64001f', + duration: 10, + sps: [new Uint8Array([0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40])], + pps: [new Uint8Array([0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0])], + width: 720, + height: 408, + pixelRatio: [1, 1], + samples, + }; +} + +function makeAudioTrack(firstPts: number, frames: number): DemuxedAudioTrack { + const samples: AudioSample[] = []; + for (let i = 0; i < frames; i++) { + samples.push({ + pts: firstPts + i * AAC_FRAME_DURATION, + unit: new Uint8Array(16), + }); + } + return { + ...dummyTrack('audio'), + type: 'audio', + id: 0, + pid: 257, + segmentCodec: 'aac', + codec: 'mp4a.40.2', + manifestCodec: 'mp4a.40.2', + duration: 10, + config: [0x11, 0x90], + samplerate: 48000, + channelCount: 2, + samples, + }; +} + +function remuxFragment( + remuxer: MP4Remuxer, + sn: number, + video: DemuxedVideoTrack, + audio: DemuxedAudioTrack, + timeOffset: number, +) { + return remuxer.remux( + audio, + video, + dummyTrack('id3') as DemuxedMetadataTrack, + dummyTrack('text') as DemuxedUserdataTrack, + timeOffset, + true, + true, + PlaylistLevelType.MAIN, + new ChunkMetadata(0, sn, 1, 0), + ); +}