diff --git a/src/remux/mp4-remuxer.ts b/src/remux/mp4-remuxer.ts index e81c6bd88fc..5491ee43fd0 100644 --- a/src/remux/mp4-remuxer.ts +++ b/src/remux/mp4-remuxer.ts @@ -100,20 +100,24 @@ export default class MP4Remuxer implements Remuxer { this.videoTrackConfig = undefined; } - getVideoStartPts(videoSamples) { + getVideoStartPts(videoSamples: VideoSample[]) { + // Get the minimum PTS value relative to the first sample's PTS, normalized for 33-bit wrapping let rolloverDetected = false; + const firstPts = videoSamples[0].pts; const startPTS = videoSamples.reduce((minPTS, sample) => { - const delta = sample.pts - minPTS; + let pts = sample.pts; + let delta = pts - minPTS; if (delta < -4294967296) { // 2^32, see PTSNormalize for reasoning, but we're hitting a rollover here, and we don't want that to impact the timeOffset calculation rolloverDetected = true; - return normalizePts(minPTS, sample.pts); - } else if (delta > 0) { + pts = normalizePts(pts, firstPts); + delta = pts - minPTS; + } + if (delta > 0) { return minPTS; - } else { - return sample.pts; } - }, videoSamples[0].pts); + return pts; + }, firstPts); if (rolloverDetected) { this.logger.debug('PTS rollover detected'); } diff --git a/tests/index.js b/tests/index.js index 1a5dfac44b5..9eb0baa2ffb 100644 --- a/tests/index.js +++ b/tests/index.js @@ -35,6 +35,7 @@ import './unit/loader/fragment-loader'; import './unit/loader/fragment'; import './unit/loader/level'; import './unit/loader/playlist-loader'; +import './unit/remux/mp4-remuxer'; import './unit/utils/attr-list'; import './unit/utils/binary-search'; import './unit/utils/buffer-helper'; diff --git a/tests/unit/remux/mp4-remuxer.ts b/tests/unit/remux/mp4-remuxer.ts new file mode 100644 index 00000000000..8c6fad26ff3 --- /dev/null +++ b/tests/unit/remux/mp4-remuxer.ts @@ -0,0 +1,93 @@ +import chai from 'chai'; +import EventEmitter from 'eventemitter3'; +import sinonChai from 'sinon-chai'; +import { hlsDefaultConfig } from '../../../src/config'; +import MP4Remuxer from '../../../src/remux/mp4-remuxer'; +import { logger } from '../../../src/utils/logger'; +import type { HlsEventEmitter } from '../../../src/events'; +import type { VideoSample } from '../../../src/types/demuxer'; +import type { TypeSupported } from '../../../src/utils/codecs'; + +chai.use(sinonChai); +const expect = chai.expect; + +describe('mp4-remuxer', function () { + let mp4Remuxer: MP4Remuxer; + + beforeEach(function () { + const observer: HlsEventEmitter = new EventEmitter() as HlsEventEmitter; + const config = { ...hlsDefaultConfig }; + const typeSupported: TypeSupported = { + mpeg: true, + mp3: true, + ac3: true, + }; + mp4Remuxer = new MP4Remuxer(observer, config, typeSupported, logger); + }); + + afterEach(function () { + mp4Remuxer.destroy(); + }); + + it('should find the lowest PTS in video samples', function () { + const videoSamples = [ptsDts(0, 0), ptsDts(3003, 3003), ptsDts(6006, 6006)]; + const minPts = mp4Remuxer.getVideoStartPts(videoSamples); + expect(minPts).to.eq(0); + }); + + it('should find the lowest PTS in video samples with decrementing PTS samples', function () { + const videoSamples = [ + ptsDts(3003, 0), + ptsDts(1001, 3003), + ptsDts(6006, 6006), + ]; + const minPts = mp4Remuxer.getVideoStartPts(videoSamples); + expect(minPts).to.eq(1001); + }); + + it('should find the lowest normalized PTS in video samples with wrapping timestamps 1/2', function () { + const videoSamples = [ + ptsDts(8589925344, 8589922341), + ptsDts(2765, 8589925344), + ptsDts(8589931350, 8589928347), + ptsDts(8589934353, 8589931350), + ptsDts(11774, 8589934353), + ptsDts(5768, 2765), + ptsDts(8771, 5768), + ptsDts(14777, 8771), + ]; + const minPts = mp4Remuxer.getVideoStartPts(videoSamples); + expect(minPts).to.eq(8589925344); + expect(minPts).to.lessThanOrEqual(8589925344); + }); + + it('should find the lowest normalized PTS in video samples with wrapping timestamps 2/2', function () { + const videoSamples = [ + ptsDts(8589931350, 8589922341), + ptsDts(8589928347, 8589928347), + ptsDts(8589934353, 8589931350), + ptsDts(11774, 8589934353), + ptsDts(5768, 2765), + ptsDts(8771, 5768), + ptsDts(14777, 8771), + ]; + const minPts = mp4Remuxer.getVideoStartPts(videoSamples); + expect(minPts).to.eq(8589928347); + }); +}); + +function ptsDts(pts: number, dts: number): VideoSample { + return { + dts, + pts, + key: true, + frame: true, + units: [ + { + data: new Uint8Array(1), + type: 0, + }, + ], + length: 1, + }; +}