MPEG-TS muxer: Write out DTS for video tracks (fixes buggy QuickTime behavior)

This commit is contained in:
Vanilagy
2026-02-16 15:30:23 +01:00
parent 2876197631
commit ddc826e6e3
4 changed files with 101 additions and 39 deletions
+6 -4
View File
@@ -26,7 +26,7 @@
chunked: true, chunked: true,
chunkSize: 2**20 chunkSize: 2**20
}); });
const outputFormat = new Mediabunny.Mp4OutputFormat({}); const outputFormat = new Mediabunny.MpegTsOutputFormat({});
const button = document.createElement('button'); const button = document.createElement('button');
button.textContent = 'Cancel'; button.textContent = 'Cancel';
@@ -58,7 +58,8 @@
}), }),
output, output,
audio: (_, n) => ({ audio: (_, n) => ({
codec: 'eac3', //codec: 'eac3',
//discard: true,
//discard: n > 1, //discard: n > 1,
//codec: 'opus', //codec: 'opus',
//codec: 'opus', //codec: 'opus',
@@ -103,7 +104,8 @@
}, },
*/ */
video: () => ({ video: () => ({
width: 320, forceTranscode: true,
width: 1280,
//forceTranscode: true, //forceTranscode: true,
//allowRotationMetadata: false, //allowRotationMetadata: false,
//width: 720, //width: 720,
@@ -186,7 +188,7 @@
}, },
trim: { trim: {
//start: 0, //start: 0,
end: 10 //end: 10
}, },
}); });
console.log(conversion); console.log(conversion);
+3
View File
@@ -72,6 +72,9 @@ import { FileSlice, readBytes, Reader, readU16Be, readU32Be, readU8 } from '../r
import { buildMpegTsMimeType, MpegTsStreamType, TIMESCALE, TS_PACKET_SIZE } from './mpeg-ts-misc'; import { buildMpegTsMimeType, MpegTsStreamType, TIMESCALE, TS_PACKET_SIZE } from './mpeg-ts-misc';
import { AC3_SAMPLE_RATES } from '../../shared/ac3-misc'; import { AC3_SAMPLE_RATES } from '../../shared/ac3-misc';
// Resources:
// ISO/IEC 13818-1
type ElementaryStream = { type ElementaryStream = {
demuxer: MpegTsDemuxer; demuxer: MpegTsDemuxer;
pid: number; pid: number;
+92 -34
View File
@@ -25,12 +25,15 @@ import {
} from '../codec-data'; } from '../codec-data';
import { assert, Bitstream, promiseWithResolvers, setUint24, toDataView, toUint8Array } from '../misc'; import { assert, Bitstream, promiseWithResolvers, setUint24, toDataView, toUint8Array } from '../misc';
import { Muxer } from '../muxer'; import { Muxer } from '../muxer';
import { Output, OutputAudioTrack, OutputVideoTrack } from '../output'; import { Output, OutputAudioTrack, OutputTrack, OutputVideoTrack } from '../output';
import { MpegTsOutputFormat } from '../output-format'; import { MpegTsOutputFormat } from '../output-format';
import { EncodedPacket } from '../packet'; import { EncodedPacket } from '../packet';
import { Writer } from '../writer'; import { Writer } from '../writer';
import { buildMpegTsMimeType, MpegTsStreamType, TIMESCALE, TS_PACKET_SIZE } from './mpeg-ts-misc'; import { buildMpegTsMimeType, MpegTsStreamType, TIMESCALE, TS_PACKET_SIZE } from './mpeg-ts-misc';
// Resources:
// ISO/IEC 13818-1
const PAT_PID = 0x0000; const PAT_PID = 0x0000;
const PMT_PID = 0x1000; const PMT_PID = 0x1000;
const FIRST_TRACK_PID = 0x0100; const FIRST_TRACK_PID = 0x0100;
@@ -47,6 +50,7 @@ type MpegTsTrackData = {
streamType: MpegTsStreamType; streamType: MpegTsStreamType;
streamId: number; streamId: number;
codecString: string; codecString: string;
timestampProcessingQueue: QueuedPacket[];
packetQueue: QueuedPacket[]; packetQueue: QueuedPacket[];
inputIsAnnexB: boolean | null; inputIsAnnexB: boolean | null;
inputIsAdts: boolean | null; inputIsAdts: boolean | null;
@@ -59,7 +63,8 @@ type MpegTsTrackData = {
type QueuedPacket = { type QueuedPacket = {
data: Uint8Array; data: Uint8Array;
timestamp: number; presentationTimestamp: number;
decodeTimestamp: number | null;
isKeyframe: boolean; isKeyframe: boolean;
}; };
@@ -77,9 +82,6 @@ export class MpegTsMuxer extends Muxer {
private videoTrackIndex = 0; private videoTrackIndex = 0;
private audioTrackIndex = 0; private audioTrackIndex = 0;
private pesHeaderBuffer = new Uint8Array(14);
private pesHeaderView = toDataView(this.pesHeaderBuffer);
private ptsBitstream = new Bitstream(this.pesHeaderBuffer.subarray(9, 14));
private adaptationFieldBuffer = new Uint8Array(184); private adaptationFieldBuffer = new Uint8Array(184);
private payloadBuffer = new Uint8Array(184); private payloadBuffer = new Uint8Array(184);
@@ -124,6 +126,7 @@ export class MpegTsMuxer extends Muxer {
streamType, streamType,
streamId, streamId,
codecString: meta.decoderConfig.codec, codecString: meta.decoderConfig.codec,
timestampProcessingQueue: [],
packetQueue: [], packetQueue: [],
inputIsAnnexB: null, inputIsAnnexB: null,
inputIsAdts: null, inputIsAdts: null,
@@ -188,6 +191,7 @@ export class MpegTsMuxer extends Muxer {
streamType, streamType,
streamId, streamId,
codecString: meta.decoderConfig.codec, codecString: meta.decoderConfig.codec,
timestampProcessingQueue: [],
packetQueue: [], packetQueue: [],
inputIsAnnexB: null, inputIsAnnexB: null,
inputIsAdts: null, inputIsAdts: null,
@@ -225,13 +229,16 @@ export class MpegTsMuxer extends Muxer {
const preparedData = this.prepareVideoPacket(trackData, packet, meta); const preparedData = this.prepareVideoPacket(trackData, packet, meta);
trackData.packetQueue.push({ if (packet.type === 'key') {
await this.flushTimestampQueue(trackData);
}
trackData.timestampProcessingQueue.push({
data: preparedData, data: preparedData,
timestamp, presentationTimestamp: timestamp,
decodeTimestamp: null,
isKeyframe: packet.type === 'key', isKeyframe: packet.type === 'key',
}); });
await this.interleavePackets();
} finally { } finally {
release(); release();
} }
@@ -255,13 +262,16 @@ export class MpegTsMuxer extends Muxer {
const preparedData = this.prepareAudioPacket(trackData, packet, meta); const preparedData = this.prepareAudioPacket(trackData, packet, meta);
trackData.packetQueue.push({ if (packet.type === 'key') {
await this.flushTimestampQueue(trackData);
}
trackData.timestampProcessingQueue.push({
data: preparedData, data: preparedData,
timestamp, presentationTimestamp: timestamp,
decodeTimestamp: null,
isKeyframe: packet.type === 'key', isKeyframe: packet.type === 'key',
}); });
await this.interleavePackets();
} finally { } finally {
release(); release();
} }
@@ -444,6 +454,28 @@ export class MpegTsMuxer extends Muxer {
return true; return true;
} }
private async flushTimestampQueue(trackData: MpegTsTrackData, alsoInterleave = true) {
if (trackData.timestampProcessingQueue.length === 0) {
return;
}
const sortedTimestamps = trackData.timestampProcessingQueue
.map(packet => packet.presentationTimestamp)
.sort((a, b) => a - b);
for (let i = 0; i < trackData.timestampProcessingQueue.length; i++) {
const queuedPacket = trackData.timestampProcessingQueue[i]!;
queuedPacket.decodeTimestamp = sortedTimestamps[i]!;
trackData.packetQueue.push(queuedPacket);
}
trackData.timestampProcessingQueue.length = 0;
if (alsoInterleave) {
await this.interleavePackets();
}
}
private async interleavePackets(isFinalCall = false) { private async interleavePackets(isFinalCall = false) {
if (!this.tablesWritten) { if (!this.tablesWritten) {
if (!this.allTracksAreKnown() && !isFinalCall) { if (!this.allTracksAreKnown() && !isFinalCall) {
@@ -469,10 +501,10 @@ export class MpegTsMuxer extends Muxer {
if ( if (
trackData.packetQueue.length > 0 trackData.packetQueue.length > 0
&& trackData.packetQueue[0]!.timestamp < minTimestamp && trackData.packetQueue[0]!.presentationTimestamp < minTimestamp
) { ) {
trackWithMinTimestamp = trackData; trackWithMinTimestamp = trackData;
minTimestamp = trackData.packetQueue[0]!.timestamp; minTimestamp = trackData.packetQueue[0]!.presentationTimestamp;
} }
} }
@@ -526,10 +558,14 @@ export class MpegTsMuxer extends Muxer {
} }
private writePesPacket(trackData: MpegTsTrackData, queuedPacket: QueuedPacket) { private writePesPacket(trackData: MpegTsTrackData, queuedPacket: QueuedPacket) {
const pesView = this.pesHeaderView; const includeDts = trackData.track.type === 'video';
const headerDataLength = includeDts ? 10 : 5;
const pesHeaderBuffer = new Uint8Array(9 + headerDataLength);
const pesView = toDataView(pesHeaderBuffer);
const ptsDtsBitstream = new Bitstream(pesHeaderBuffer.subarray(9));
setUint24(pesView, 0, 0x000001, false); // packet_start_code_prefix setUint24(pesView, 0, 0x000001, false); // packet_start_code_prefix
this.pesHeaderBuffer[3] = trackData.streamId; // stream_id pesHeaderBuffer[3] = trackData.streamId; // stream_id
const pesPacketLength = trackData.track.type === 'video' const pesPacketLength = trackData.track.type === 'video'
? 0 // Unbounded ? 0 // Unbounded
@@ -539,20 +575,33 @@ export class MpegTsMuxer extends Muxer {
// '10' marker, PES_scrambling_control=0, PES_priority=0, // '10' marker, PES_scrambling_control=0, PES_priority=0,
// data_alignment_indicator=1, copyright=0, original_or_copy=0 // data_alignment_indicator=1, copyright=0, original_or_copy=0
pesView.setUint8(6, 0x84); pesView.setUint8(6, 0x84);
pesView.setUint8(7, 0x80); // PTS_DTS_flags=10 (PTS only), other flags=0 pesView.setUint8(7, includeDts ? 0xC0 : 0x80); // PTS_DTS_flags, other flags=0
pesView.setUint8(8, 5); // PES_header_data_length (5 bytes for PTS) pesView.setUint8(8, headerDataLength); // PES_header_data_length
const pts = Math.round(queuedPacket.timestamp * TIMESCALE); const pts = Math.round(queuedPacket.presentationTimestamp * TIMESCALE);
this.ptsBitstream.pos = 0; ptsDtsBitstream.pos = 0;
this.ptsBitstream.writeBits(4, 0b0010); // marker ptsDtsBitstream.writeBits(4, includeDts ? 0b0011 : 0b0010); // marker
this.ptsBitstream.writeBits(3, (pts >>> 30) & 0x7); // PTS[32:30] ptsDtsBitstream.writeBits(3, (pts >>> 30) & 0x7); // PTS[32:30]
this.ptsBitstream.writeBits(1, 1); // marker_bit ptsDtsBitstream.writeBits(1, 1); // marker_bit
this.ptsBitstream.writeBits(15, (pts >>> 15) & 0x7FFF); // PTS[29:15] ptsDtsBitstream.writeBits(15, (pts >>> 15) & 0x7FFF); // PTS[29:15]
this.ptsBitstream.writeBits(1, 1); // marker_bit ptsDtsBitstream.writeBits(1, 1); // marker_bit
this.ptsBitstream.writeBits(15, pts & 0x7FFF); // PTS[14:0] ptsDtsBitstream.writeBits(15, pts & 0x7FFF); // PTS[14:0]
this.ptsBitstream.writeBits(1, 1); // marker_bit ptsDtsBitstream.writeBits(1, 1); // marker_bit
const totalLength = this.pesHeaderBuffer.length + queuedPacket.data.length; if (includeDts) {
assert(queuedPacket.decodeTimestamp !== null);
const dts = Math.round(queuedPacket.decodeTimestamp * TIMESCALE);
ptsDtsBitstream.writeBits(4, 0b0001);
ptsDtsBitstream.writeBits(3, (dts >>> 30) & 0x7); // DTS[32:30]
ptsDtsBitstream.writeBits(1, 1); // marker_bit
ptsDtsBitstream.writeBits(15, (dts >>> 15) & 0x7FFF); // DTS[29:15]
ptsDtsBitstream.writeBits(1, 1); // marker_bit
ptsDtsBitstream.writeBits(15, dts & 0x7FFF); // DTS[14:0]
ptsDtsBitstream.writeBits(1, 1); // marker_bit
}
const totalLength = pesHeaderBuffer.length + queuedPacket.data.length;
let offset = 0; let offset = 0;
let isFirstTsPacket = true; let isFirstTsPacket = true;
@@ -593,13 +642,13 @@ export class MpegTsMuxer extends Muxer {
const payload = this.payloadBuffer.subarray(0, payloadSize); const payload = this.payloadBuffer.subarray(0, payloadSize);
let payloadOffset = 0; let payloadOffset = 0;
if (offset < this.pesHeaderBuffer.length) { if (offset < pesHeaderBuffer.length) {
const headerBytes = Math.min(this.pesHeaderBuffer.length - offset, payloadSize); const headerBytes = Math.min(pesHeaderBuffer.length - offset, payloadSize);
payload.set(this.pesHeaderBuffer.subarray(offset, offset + headerBytes), 0); payload.set(pesHeaderBuffer.subarray(offset, offset + headerBytes), 0);
payloadOffset = headerBytes; payloadOffset = headerBytes;
} }
const dataStart = Math.max(0, offset - this.pesHeaderBuffer.length); const dataStart = Math.max(0, offset - pesHeaderBuffer.length);
const dataEnd = dataStart + (payloadSize - payloadOffset); const dataEnd = dataStart + (payloadSize - payloadOffset);
if (payloadOffset < payloadSize) { if (payloadOffset < payloadSize) {
payload.set(queuedPacket.data.subarray(dataStart, dataEnd), payloadOffset); payload.set(queuedPacket.data.subarray(dataStart, dataEnd), payloadOffset);
@@ -658,13 +707,18 @@ export class MpegTsMuxer extends Muxer {
} }
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
override async onTrackClose() { override async onTrackClose(track: OutputTrack) {
const release = await this.mutex.acquire(); const release = await this.mutex.acquire();
if (this.allTracksAreKnown()) { if (this.allTracksAreKnown()) {
this.allTracksKnown.resolve(); this.allTracksKnown.resolve();
} }
const trackData = this.trackDatas.find(x => x.track === track);
if (trackData) {
await this.flushTimestampQueue(trackData, false);
}
await this.interleavePackets(); await this.interleavePackets();
release(); release();
@@ -675,6 +729,10 @@ export class MpegTsMuxer extends Muxer {
this.allTracksKnown.resolve(); this.allTracksKnown.resolve();
for (const trackData of this.trackDatas) {
await this.flushTimestampQueue(trackData, false);
}
await this.interleavePackets(true); await this.interleavePackets(true);
release(); release();
-1
View File
@@ -1 +0,0 @@
- Investigate quicktime stuttering with mediabunny-generated .ts file