From 8fea9671ea82d0621f0142ea632dc69f91ef2cc1 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 15 Jan 2026 08:51:07 +0100 Subject: [PATCH] Fix some demuxer bugs that occurred on files by FFmpeg, fix packet marking logic --- src/mpeg-ts/mpeg-ts-demuxer.ts | 25 +++++++++++++++++-------- test/node/mpeg-ts-demuxing.test.ts | 21 ++++++++++++++++++++- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/mpeg-ts/mpeg-ts-demuxer.ts b/src/mpeg-ts/mpeg-ts-demuxer.ts index d405146..4eaba45 100644 --- a/src/mpeg-ts/mpeg-ts-demuxer.ts +++ b/src/mpeg-ts/mpeg-ts-demuxer.ts @@ -135,8 +135,9 @@ export class MpegTsDemuxer extends Demuxer { } let currentPos = this.packetOffset; - let programMapPid: number | null = null; + // Some files contain these multiple times, but we only care about their first appearance + let hasProgramAssociationTable = false; let hasProgramMap = false; while (true) { @@ -148,7 +149,7 @@ export class MpegTsDemuxer extends Demuxer { const BYTES_BEFORE_SECTION_LENGTH = 3; const BITS_IN_CRC_32 = 32; // Duh - if (section.pid === 0) { + if (section.pid === 0 && !hasProgramAssociationTable) { const bitstream = new Bitstream(section.payload); const pointerField = bitstream.readAlignedByte(); @@ -175,7 +176,9 @@ export class MpegTsDemuxer extends Demuxer { if (programMapPid === null) { throw new Error('Program Association Table must link to a Program Map Table.'); } - } else if (section.pid === programMapPid) { + + hasProgramAssociationTable = true; + } else if (section.pid === programMapPid && !hasProgramMap) { const bitstream = new Bitstream(section.payload); const pointerField = bitstream.readAlignedByte(); @@ -1258,8 +1261,9 @@ class MpegTsVideoTrackBacking extends MpegTsTrackBacking implements InputVideoTr let remaining = context.ensureBuffered(CHUNK_SIZE); if (remaining instanceof Promise) remaining = await remaining; - // Search for start codes in the current chunk - for (let i = 0; i < remaining; i++) { + const startPos = context.currentPos; + + while (context.currentPos - startPos < remaining) { const byte = context.readU8(); // Look for 0x00 as potential start of a start code @@ -1281,6 +1285,7 @@ class MpegTsVideoTrackBacking extends MpegTsTrackBacking implements InputVideoTr context.seekTo(packetStartPos); return context.supplyPacket(packetLength, 0); } + return; } @@ -1394,14 +1399,16 @@ class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTr let remaining = context.ensureBuffered(CHUNK_SIZE); if (remaining instanceof Promise) remaining = await remaining; - for (let i = 0; i < remaining; i++) { + const startPos = context.currentPos; + + while (context.currentPos - startPos < remaining) { const byte = context.readU8(); if (byte !== 0xff) { continue; } context.skip(-1); - const startPos = context.currentPos; + const possibleHeaderStartPos = context.currentPos; let remaining = context.ensureBuffered(MAX_FRAME_HEADER_SIZE); if (remaining instanceof Promise) remaining = await remaining; @@ -1414,7 +1421,7 @@ class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTr const header = readAdtsFrameHeader(FileSlice.tempFromBytes(headerBytes)); if (header) { - context.seekTo(startPos); + context.seekTo(possibleHeaderStartPos); let remaining = context.ensureBuffered(header.frameLength); if (remaining instanceof Promise) remaining = await remaining; @@ -1423,6 +1430,8 @@ class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTr remaining, Math.round(SAMPLES_PER_AAC_FRAME * TIMESCALE / this.elementaryStream.info.sampleRate), ); + } else { + context.seekTo(possibleHeaderStartPos + 1); } } diff --git a/test/node/mpeg-ts-demuxing.test.ts b/test/node/mpeg-ts-demuxing.test.ts index 50c7486..aced224 100644 --- a/test/node/mpeg-ts-demuxing.test.ts +++ b/test/node/mpeg-ts-demuxing.test.ts @@ -1,6 +1,6 @@ import { expect, test } from 'vitest'; import { Input } from '../../src/input.js'; -import { FilePathSource, ReadableStreamSource } from '../../src/source.js'; +import { FilePathSource, ReadableStreamSource, UrlSource } from '../../src/source.js'; import path from 'node:path'; import fs from 'node:fs'; import { Readable } from 'node:stream'; @@ -450,3 +450,22 @@ test('MPEG-TS with unknown file size (ReadableStreamSource)', async () => { expect((videoTrack._backing as unknown as MpegTsTrackBacking).referencePesPackets.length) .toBeGreaterThanOrEqual(10); }); + +test('MPEG-TS transmuxed by FFmpeg', async () => { + using input = new Input({ + source: new UrlSource('https://pub-cf9fcfcb5c0a44e9b1bb5ff890e041ae.r2.dev/trim-buck-bunny-ffmpeg.ts'), + formats: ALL_FORMATS, + }); + + const videoTrack = await input.getPrimaryVideoTrack(); + assert(videoTrack); + + const audioTrack = await input.getPrimaryAudioTrack(); + assert(audioTrack); + + const videoPacketStats = await videoTrack.computePacketStats(); + const audioPacketStats = await audioTrack.computePacketStats(); + + expect(videoPacketStats.packetCount).toBe(121); + expect(audioPacketStats.packetCount).toBe(235); +});