diff --git a/package-lock.json b/package-lock.json index 36397ee..485a15a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.29.0", + "version": "1.29.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.29.0", + "version": "1.29.1", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -7739,9 +7739,9 @@ } }, "node_modules/mediabunny": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.28.0.tgz", - "integrity": "sha512-D63nzvBRIBSUsRgaIfFugWCy2iOV5T/C6nHn2fW0aWqyRuSGzWsVMXzlNi3iCKieoA/WECYJg8oVGtUukpy3XQ==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.29.0.tgz", + "integrity": "sha512-18B8w/rhO/ph/AFsIXvzZg8RaSQZ+ZYfJ99MZlTjDmlgCT58jV3azrnWQ/OSquYDi8q0xmn64mnfTEHgww3+zw==", "license": "MPL-2.0", "peer": true, "workspaces": [ @@ -12065,7 +12065,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.29.0", + "version": "1.29.1", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" diff --git a/package.json b/package.json index 1bf2e6d..cc3718c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.29.0", + "version": "1.29.1", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "type": "module", "workspaces": [ diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json index e065305..8968b74 100644 --- a/packages/mp3-encoder/package.json +++ b/packages/mp3-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/mp3-encoder", "author": "Vanilagy", - "version": "1.29.0", + "version": "1.29.1", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs", diff --git a/src/mpeg-ts/mpeg-ts-demuxer.ts b/src/mpeg-ts/mpeg-ts-demuxer.ts index 17307c6..dc2a25c 100644 --- a/src/mpeg-ts/mpeg-ts-demuxer.ts +++ b/src/mpeg-ts/mpeg-ts-demuxer.ts @@ -46,6 +46,7 @@ import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata'; import { assert, AsyncMutex, + binarySearchExact, binarySearchLessOrEqual, Bitstream, COLOR_PRIMARIES_MAP_INVERSE, @@ -118,6 +119,7 @@ export class MpegTsDemuxer extends Demuxer { tracks: InputTrack[] = []; packetOffset = 0; packetStride = -1; + sectionEndPositions: number[] = []; constructor(input: Input) { super(input); @@ -157,7 +159,11 @@ export class MpegTsDemuxer extends Demuxer { let hasProgramMap = false; while (true) { - const section = await this.readSection(currentPos, true); + const section = await this.readSection( + currentPos, + true, + !hasProgramMap, // Expect contiguous sections as long as we don't have the PMT + ); if (!section) { break; } @@ -447,13 +453,13 @@ export class MpegTsDemuxer extends Demuxer { return buildMpegTsMimeType(codecStrings); } - async readSection(startPos: number, full: boolean): Promise
{ + async readSection(startPos: number, full: boolean, contiguous = false): Promise
{ let endPos = startPos; let currentPos = startPos; const chunks: Uint8Array[] = []; let chunksByteLength = 0; - let firstPacket: TsPacket | null = null; + let mustAddSectionEnd = true; while (true) { const packet = await this.readPacket(currentPos); @@ -471,7 +477,11 @@ export class MpegTsDemuxer extends Demuxer { firstPacket = packet; } else { if (packet.pid !== firstPacket.pid) { - continue; // Ignore this packet + if (contiguous) { + break; // End of section + } else { + continue; // Ignore this packet + } } if (packet.payloadUnitStartIndicator === 1) { @@ -501,8 +511,21 @@ export class MpegTsDemuxer extends Demuxer { // 64 is just "a bit of data", enough for the PES packet header if (!full && chunksByteLength >= 64) { + mustAddSectionEnd = false; // Not the actual section end break; } + + // Check if we already know this is a section end + const isKnownSectionEnd = binarySearchExact(this.sectionEndPositions, endPos, x => x) !== -1; + if (isKnownSectionEnd) { + mustAddSectionEnd = false; + break; + } + } + + if (mustAddSectionEnd) { + const index = binarySearchLessOrEqual(this.sectionEndPositions, endPos, x => x); + this.sectionEndPositions.splice(index + 1, 0, endPos); } if (!firstPacket) { diff --git a/test/node/mpeg-ts-demuxing.test.ts b/test/node/mpeg-ts-demuxing.test.ts index 4afe629..d4d1a1f 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, UrlSource } from '../../src/source.js'; +import { FilePathSource, ReadableStreamSource, StreamSource, UrlSource } from '../../src/source.js'; import path from 'node:path'; import fs from 'node:fs'; import { Readable } from 'node:stream'; @@ -241,11 +241,11 @@ test('MPEG-TS video seeking', async () => { expect(allPackets).toHaveLength(298); for (const packet of allPackets) { - const seekedPacked = await sink.getPacket(packet.timestamp); - assert(seekedPacked); - expect(seekedPacked.timestamp).toBe(packet.timestamp); // The correct timestamp was retrieved for this packet - expect(seekedPacked.duration).toBe(packet.duration); // The correct duration was retrieved for this packet - expect(seekedPacked.sequenceNumber).toBe(packet.sequenceNumber); + const seekedPacket = await sink.getPacket(packet.timestamp); + assert(seekedPacket); + expect(seekedPacket.timestamp).toBe(packet.timestamp); // The correct timestamp was retrieved for this packet + expect(seekedPacket.duration).toBe(packet.duration); // The correct duration was retrieved for this packet + expect(seekedPacket.sequenceNumber).toBe(packet.sequenceNumber); } }); @@ -573,3 +573,27 @@ test('MPEG-TS with MP3 audio', async () => { expect(count).toBeGreaterThan(0); }); + +test('MPEG-TS partial reading', async () => { + const fullPath = path.join(__dirname, '../public/193039199_mp4_h264_aac_fhd_7.ts'); + const buffer = await fs.promises.readFile(fullPath); + let maxEnd = 0; + + using input = new Input({ + source: new StreamSource({ + getSize: () => { + return buffer.byteLength; + }, + read: (start, end) => { + maxEnd = Math.max(maxEnd, end); + return buffer.subarray(start, end); + }, + }), + formats: ALL_FORMATS, + }); + + await input.getTracks(); + + // Not much of the file has been read since we only requested metadata + expect(maxEnd).toBeLessThanOrEqual(86480); +});