@invintusmedia/tomp4 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -80,13 +80,68 @@ export class TSMuxer {
80
80
  this.pendingAudio.push({ data: new Uint8Array(adtsData), pts: pts90k });
81
81
  }
82
82
 
83
+ /**
84
+ * Add H.264 video sample from NAL units (Annex B format)
85
+ * Used when re-muxing parsed MPEG-TS data
86
+ * @param {Uint8Array[]} nalUnits - Array of NAL units (without start codes)
87
+ * @param {boolean} isKey - Is this a keyframe
88
+ * @param {number} pts90k - Presentation timestamp in 90kHz ticks
89
+ * @param {number} [dts90k] - Decode timestamp in 90kHz ticks (defaults to pts90k)
90
+ */
91
+ addVideoNalUnits(nalUnits, isKey, pts90k, dts90k = pts90k) {
92
+ const parts = [];
93
+
94
+ // Add AUD (Access Unit Delimiter) at start of each access unit
95
+ parts.push(new Uint8Array([0, 0, 0, 1, 0x09, 0xF0]));
96
+
97
+ // If keyframe, prepend SPS/PPS
98
+ if (isKey && this.sps && this.pps) {
99
+ parts.push(new Uint8Array([0, 0, 0, 1]));
100
+ parts.push(this.sps);
101
+ parts.push(new Uint8Array([0, 0, 0, 1]));
102
+ parts.push(this.pps);
103
+ }
104
+
105
+ // Add each NAL unit with start code
106
+ for (const nalUnit of nalUnits) {
107
+ // Skip SPS/PPS from source if we're adding our own
108
+ const nalType = nalUnit[0] & 0x1F;
109
+ if (isKey && this.sps && this.pps && (nalType === 7 || nalType === 8)) {
110
+ continue;
111
+ }
112
+ parts.push(new Uint8Array([0, 0, 0, 1]));
113
+ parts.push(nalUnit);
114
+ }
115
+
116
+ // Build PES packet
117
+ const annexB = concat(parts);
118
+ const pes = this._buildVideoPES(annexB, pts90k, dts90k);
119
+
120
+ // Write PAT/PMT before keyframes
121
+ if (isKey) {
122
+ this.packets.push(this._buildPAT());
123
+ this.packets.push(this._buildPMT());
124
+ }
125
+
126
+ // Write pending audio with PTS <= this video frame
127
+ while (this.pendingAudio.length > 0 && this.pendingAudio[0].pts <= pts90k) {
128
+ const audio = this.pendingAudio.shift();
129
+ const audioPes = this._buildAudioPES(audio.data, audio.pts);
130
+ this._packetizePES(audioPes, 0x102, false, audio.pts, 'audio');
131
+ }
132
+
133
+ // Packetize video PES into 188-byte TS packets
134
+ this._packetizePES(pes, 0x101, isKey, dts90k, 'video');
135
+ }
136
+
83
137
  /**
84
138
  * Add H.264 video sample from WebCodecs encoder (AVCC format with length prefixes)
85
139
  * @param {Uint8Array} avccData - AVCC-formatted NAL units
86
140
  * @param {boolean} isKey - Is this a keyframe
87
141
  * @param {number} pts90k - Presentation timestamp in 90kHz ticks
142
+ * @param {number} [dts90k] - Decode timestamp in 90kHz ticks (defaults to pts90k)
88
143
  */
89
- addVideoSample(avccData, isKey, pts90k) {
144
+ addVideoSample(avccData, isKey, pts90k, dts90k = pts90k) {
90
145
  const nalUnits = [];
91
146
 
92
147
  // Add AUD (Access Unit Delimiter) at start of each access unit
@@ -102,20 +157,28 @@ export class TSMuxer {
102
157
 
103
158
  // Parse AVCC NALs and convert to Annex B
104
159
  let offset = 0;
105
- while (offset < avccData.length - 4) {
160
+ let iterations = 0;
161
+ const maxIterations = 10000;
162
+
163
+ while (offset + 4 <= avccData.length && iterations < maxIterations) {
164
+ iterations++;
106
165
  const len = (avccData[offset] << 24) | (avccData[offset + 1] << 16) |
107
166
  (avccData[offset + 2] << 8) | avccData[offset + 3];
108
167
  offset += 4;
109
- if (len > 0 && offset + len <= avccData.length) {
110
- nalUnits.push(new Uint8Array([0, 0, 0, 1]));
111
- nalUnits.push(avccData.slice(offset, offset + len));
168
+
169
+ // Safety: bail on invalid NAL length
170
+ if (len <= 0 || len > avccData.length - offset) {
171
+ break;
112
172
  }
173
+
174
+ nalUnits.push(new Uint8Array([0, 0, 0, 1]));
175
+ nalUnits.push(avccData.slice(offset, offset + len));
113
176
  offset += len;
114
177
  }
115
178
 
116
- // Build PES packet
179
+ // Build PES packet with both PTS and DTS
117
180
  const annexB = concat(nalUnits);
118
- const pes = this._buildVideoPES(annexB, pts90k);
181
+ const pes = this._buildVideoPES(annexB, pts90k, dts90k);
119
182
 
120
183
  // Write PAT/PMT before keyframes
121
184
  if (isKey) {
@@ -130,8 +193,8 @@ export class TSMuxer {
130
193
  this._packetizePES(audioPes, 0x102, false, audio.pts, 'audio');
131
194
  }
132
195
 
133
- // Packetize video PES into 188-byte TS packets
134
- this._packetizePES(pes, 0x101, isKey, pts90k, 'video');
196
+ // Packetize video PES into 188-byte TS packets (use DTS for PCR)
197
+ this._packetizePES(pes, 0x101, isKey, dts90k, 'video');
135
198
  }
136
199
 
137
200
  /**
@@ -160,16 +223,32 @@ export class TSMuxer {
160
223
 
161
224
  // --- Private methods ---
162
225
 
163
- _buildVideoPES(payload, pts90k) {
164
- const pes = new Uint8Array(14 + payload.length);
226
+ _buildVideoPES(payload, pts90k, dts90k) {
227
+ // If PTS == DTS, only write PTS (saves 5 bytes per frame)
228
+ const hasDts = pts90k !== dts90k;
229
+ const headerLen = hasDts ? 10 : 5;
230
+ const pes = new Uint8Array(9 + headerLen + payload.length);
231
+
165
232
  pes[0] = 0; pes[1] = 0; pes[2] = 1; // Start code
166
233
  pes[3] = 0xE0; // Stream ID (video)
167
234
  pes[4] = 0; pes[5] = 0; // Length = 0 (unbounded)
168
- pes[6] = 0x80; // Flags
169
- pes[7] = 0x80; // PTS present
170
- pes[8] = 5; // Header length
171
- this._writePTS(pes, 9, pts90k, 0x21);
172
- pes.set(payload, 14);
235
+ pes[6] = 0x80; // Flags: data_alignment
236
+
237
+ if (hasDts) {
238
+ // PTS + DTS present
239
+ pes[7] = 0xC0; // PTS_DTS_flags = 11
240
+ pes[8] = 10; // Header length: 5 (PTS) + 5 (DTS)
241
+ this._writePTS(pes, 9, pts90k, 0x31); // PTS marker = 0011
242
+ this._writePTS(pes, 14, dts90k, 0x11); // DTS marker = 0001
243
+ pes.set(payload, 19);
244
+ } else {
245
+ // PTS only
246
+ pes[7] = 0x80; // PTS_DTS_flags = 10
247
+ pes[8] = 5; // Header length: 5 (PTS)
248
+ this._writePTS(pes, 9, pts90k, 0x21); // PTS marker = 0010
249
+ pes.set(payload, 14);
250
+ }
251
+
173
252
  return pes;
174
253
  }
175
254
 
@@ -220,14 +299,17 @@ export class TSMuxer {
220
299
 
221
300
  pkt[3] = 0x30 | (this.cc[cc] & 0x0F);
222
301
  pkt[4] = afLen;
223
- pkt[5] = 0x50; // PCR + random_access
302
+ pkt[5] = 0x50; // PCR flag + random_access_indicator
303
+
304
+ // PCR = 33-bit base (90kHz) + 6 reserved bits + 9-bit extension (27MHz)
305
+ // We only use the base, extension = 0
224
306
  const pcrBase = BigInt(pts90k);
225
307
  pkt[6] = Number((pcrBase >> 25n) & 0xFFn);
226
308
  pkt[7] = Number((pcrBase >> 17n) & 0xFFn);
227
309
  pkt[8] = Number((pcrBase >> 9n) & 0xFFn);
228
310
  pkt[9] = Number((pcrBase >> 1n) & 0xFFn);
229
- pkt[10] = (Number(pcrBase & 1n) << 7) | 0x7E;
230
- pkt[11] = 0;
311
+ pkt[10] = (Number(pcrBase & 1n) << 7) | 0x7E; // LSB of base + 6 reserved (111111)
312
+ pkt[11] = 0; // 9-bit extension = 0
231
313
 
232
314
  pkt.set(pes.slice(offset, offset + payloadLen), 12);
233
315
  offset += payloadLen;