@invintusmedia/tomp4 1.4.0 → 1.4.2
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.
- package/dist/tomp4.js +2 -2
- package/package.json +1 -1
- package/src/codecs/h264-cavlc-tables.js +628 -0
- package/src/codecs/h264-encoder.js +63 -138
- package/src/hls-clip.js +246 -106
- package/src/index.js +1 -1
package/dist/tomp4.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* toMp4.js v1.4.
|
|
2
|
+
* toMp4.js v1.4.2
|
|
3
3
|
* Convert MPEG-TS and fMP4 to standard MP4
|
|
4
4
|
* https://github.com/TVWIT/toMp4.js
|
|
5
5
|
* MIT License
|
|
@@ -1186,7 +1186,7 @@
|
|
|
1186
1186
|
toMp4.isMpegTs = isMpegTs;
|
|
1187
1187
|
toMp4.isFmp4 = isFmp4;
|
|
1188
1188
|
toMp4.isStandardMp4 = isStandardMp4;
|
|
1189
|
-
toMp4.version = '1.4.
|
|
1189
|
+
toMp4.version = '1.4.2';
|
|
1190
1190
|
|
|
1191
1191
|
return toMp4;
|
|
1192
1192
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* H.264 CAVLC (Context-Adaptive Variable-Length Coding) Encoding Tables
|
|
3
|
+
*
|
|
4
|
+
* Complete VLC tables from ITU-T H.264 specification:
|
|
5
|
+
* - Table 9-5: coeff_token VLC tables (5 nC ranges + ChromaDC variants)
|
|
6
|
+
* - Table 9-7: total_zeros for 4x4 blocks (TotalCoeff 1-15)
|
|
7
|
+
* - Table 9-8: total_zeros for chroma DC 2x2 (TotalCoeff 1-3)
|
|
8
|
+
* - Table 9-9: total_zeros for chroma DC 2x4 (TotalCoeff 1-7)
|
|
9
|
+
* - Table 9-10: run_before (zerosLeft 1-7+)
|
|
10
|
+
*
|
|
11
|
+
* Each entry is [bits, length] where:
|
|
12
|
+
* - bits: the VLC codeword value (MSB-first integer)
|
|
13
|
+
* - length: number of bits in the codeword
|
|
14
|
+
*
|
|
15
|
+
* Source: Verified against x264 (common/tables.c) and FFmpeg (libavcodec/h264_cavlc.c).
|
|
16
|
+
*
|
|
17
|
+
* @module codecs/h264-cavlc-tables
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
21
|
+
// coeff_token for TotalCoeff=0 (no coefficients)
|
|
22
|
+
// Indexed by nC table index (0-5):
|
|
23
|
+
// 0: nC 0-1, 1: nC 2-3, 2: nC 4-7, 3: nC 8+, 4: nC=-1 (ChromaDC 2x2), 5: nC=-2 (ChromaDC 2x4)
|
|
24
|
+
// Table 9-5 in the spec
|
|
25
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
export const coeff0Token = [
|
|
28
|
+
[0x01, 1], // nC 0-1: "1"
|
|
29
|
+
[0x03, 2], // nC 2-3: "11"
|
|
30
|
+
[0x0f, 4], // nC 4-7: "1111"
|
|
31
|
+
[0x03, 6], // nC 8+: "000011"
|
|
32
|
+
[0x01, 2], // ChromaDC 2x2: "01"
|
|
33
|
+
[0x01, 1], // ChromaDC 2x4: "1"
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
37
|
+
// coeff_token VLC tables
|
|
38
|
+
// coeffTokenTable[nC_table][totalCoeff-1][trailingOnes] = [bits, length]
|
|
39
|
+
//
|
|
40
|
+
// nC table index mapping:
|
|
41
|
+
// 0: nC = 0,1 (Table 9-5a)
|
|
42
|
+
// 1: nC = 2,3 (Table 9-5b)
|
|
43
|
+
// 2: nC = 4,5,6,7 (Table 9-5c)
|
|
44
|
+
// 3: nC >= 8 (Table 9-5d) -- fixed 6-bit codes
|
|
45
|
+
// 4: nC = -1 (Chroma DC 2x2, Table 9-5e)
|
|
46
|
+
// 5: nC = -2 (Chroma DC 2x4/4x2, for 4:2:2)
|
|
47
|
+
//
|
|
48
|
+
// trailingOnes: 0..3 (but only 0..min(totalCoeff,3) are valid)
|
|
49
|
+
// Invalid combinations have [0, 0] placeholder
|
|
50
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
51
|
+
|
|
52
|
+
export const coeffTokenTable = [
|
|
53
|
+
// ── nC = 0,1 (table 0) ──
|
|
54
|
+
[
|
|
55
|
+
// totalCoeff=1
|
|
56
|
+
[[0x05, 6], [0x01, 2], [0, 0], [0, 0]],
|
|
57
|
+
// totalCoeff=2
|
|
58
|
+
[[0x07, 8], [0x04, 6], [0x01, 3], [0, 0]],
|
|
59
|
+
// totalCoeff=3
|
|
60
|
+
[[0x07, 9], [0x06, 8], [0x05, 7], [0x03, 5]],
|
|
61
|
+
// totalCoeff=4
|
|
62
|
+
[[0x07, 10], [0x06, 9], [0x05, 8], [0x03, 6]],
|
|
63
|
+
// totalCoeff=5
|
|
64
|
+
[[0x07, 11], [0x06, 10], [0x05, 9], [0x04, 7]],
|
|
65
|
+
// totalCoeff=6
|
|
66
|
+
[[0x0f, 13], [0x06, 11], [0x05, 10], [0x04, 8]],
|
|
67
|
+
// totalCoeff=7
|
|
68
|
+
[[0x0b, 13], [0x0e, 13], [0x05, 11], [0x04, 9]],
|
|
69
|
+
// totalCoeff=8
|
|
70
|
+
[[0x08, 13], [0x0a, 13], [0x0d, 13], [0x04, 10]],
|
|
71
|
+
// totalCoeff=9
|
|
72
|
+
[[0x0f, 14], [0x0e, 14], [0x09, 13], [0x04, 11]],
|
|
73
|
+
// totalCoeff=10
|
|
74
|
+
[[0x0b, 14], [0x0a, 14], [0x0d, 14], [0x0c, 13]],
|
|
75
|
+
// totalCoeff=11
|
|
76
|
+
[[0x0f, 15], [0x0e, 15], [0x09, 14], [0x0c, 14]],
|
|
77
|
+
// totalCoeff=12
|
|
78
|
+
[[0x0b, 15], [0x0a, 15], [0x0d, 15], [0x08, 14]],
|
|
79
|
+
// totalCoeff=13
|
|
80
|
+
[[0x0f, 16], [0x01, 15], [0x09, 15], [0x0c, 15]],
|
|
81
|
+
// totalCoeff=14
|
|
82
|
+
[[0x0b, 16], [0x0e, 16], [0x0d, 16], [0x08, 15]],
|
|
83
|
+
// totalCoeff=15
|
|
84
|
+
[[0x07, 16], [0x0a, 16], [0x09, 16], [0x0c, 16]],
|
|
85
|
+
// totalCoeff=16
|
|
86
|
+
[[0x04, 16], [0x06, 16], [0x05, 16], [0x08, 16]],
|
|
87
|
+
],
|
|
88
|
+
|
|
89
|
+
// ── nC = 2,3 (table 1) ──
|
|
90
|
+
[
|
|
91
|
+
// totalCoeff=1
|
|
92
|
+
[[0x0b, 6], [0x02, 2], [0, 0], [0, 0]],
|
|
93
|
+
// totalCoeff=2
|
|
94
|
+
[[0x07, 6], [0x07, 5], [0x03, 3], [0, 0]],
|
|
95
|
+
// totalCoeff=3
|
|
96
|
+
[[0x07, 7], [0x0a, 6], [0x09, 6], [0x05, 4]],
|
|
97
|
+
// totalCoeff=4
|
|
98
|
+
[[0x07, 8], [0x06, 6], [0x05, 6], [0x04, 4]],
|
|
99
|
+
// totalCoeff=5
|
|
100
|
+
[[0x04, 8], [0x06, 7], [0x05, 7], [0x06, 5]],
|
|
101
|
+
// totalCoeff=6
|
|
102
|
+
[[0x07, 9], [0x06, 8], [0x05, 8], [0x08, 6]],
|
|
103
|
+
// totalCoeff=7
|
|
104
|
+
[[0x0f, 11], [0x06, 9], [0x05, 9], [0x04, 6]],
|
|
105
|
+
// totalCoeff=8
|
|
106
|
+
[[0x0b, 11], [0x0e, 11], [0x0d, 11], [0x04, 7]],
|
|
107
|
+
// totalCoeff=9
|
|
108
|
+
[[0x0f, 12], [0x0a, 11], [0x09, 11], [0x04, 9]],
|
|
109
|
+
// totalCoeff=10
|
|
110
|
+
[[0x0b, 12], [0x0e, 12], [0x0d, 12], [0x0c, 11]],
|
|
111
|
+
// totalCoeff=11
|
|
112
|
+
[[0x08, 12], [0x0a, 12], [0x09, 12], [0x08, 11]],
|
|
113
|
+
// totalCoeff=12
|
|
114
|
+
[[0x0f, 13], [0x0e, 13], [0x0d, 13], [0x0c, 12]],
|
|
115
|
+
// totalCoeff=13
|
|
116
|
+
[[0x0b, 13], [0x0a, 13], [0x09, 13], [0x0c, 13]],
|
|
117
|
+
// totalCoeff=14
|
|
118
|
+
[[0x07, 13], [0x0b, 14], [0x06, 13], [0x08, 13]],
|
|
119
|
+
// totalCoeff=15
|
|
120
|
+
[[0x09, 14], [0x08, 14], [0x0a, 14], [0x01, 13]],
|
|
121
|
+
// totalCoeff=16
|
|
122
|
+
[[0x07, 14], [0x06, 14], [0x05, 14], [0x04, 14]],
|
|
123
|
+
],
|
|
124
|
+
|
|
125
|
+
// ── nC = 4,5,6,7 (table 2) ──
|
|
126
|
+
[
|
|
127
|
+
// totalCoeff=1
|
|
128
|
+
[[0x0f, 6], [0x0e, 4], [0, 0], [0, 0]],
|
|
129
|
+
// totalCoeff=2
|
|
130
|
+
[[0x0b, 6], [0x0f, 5], [0x0d, 4], [0, 0]],
|
|
131
|
+
// totalCoeff=3
|
|
132
|
+
[[0x08, 6], [0x0c, 5], [0x0e, 5], [0x0c, 4]],
|
|
133
|
+
// totalCoeff=4
|
|
134
|
+
[[0x0f, 7], [0x0a, 5], [0x0b, 5], [0x0b, 4]],
|
|
135
|
+
// totalCoeff=5
|
|
136
|
+
[[0x0b, 7], [0x08, 5], [0x09, 5], [0x0a, 4]],
|
|
137
|
+
// totalCoeff=6
|
|
138
|
+
[[0x09, 7], [0x0e, 6], [0x0d, 6], [0x09, 4]],
|
|
139
|
+
// totalCoeff=7
|
|
140
|
+
[[0x08, 7], [0x0a, 6], [0x09, 6], [0x08, 4]],
|
|
141
|
+
// totalCoeff=8
|
|
142
|
+
[[0x0f, 8], [0x0e, 7], [0x0d, 7], [0x0d, 5]],
|
|
143
|
+
// totalCoeff=9
|
|
144
|
+
[[0x0b, 8], [0x0e, 8], [0x0a, 7], [0x0c, 6]],
|
|
145
|
+
// totalCoeff=10
|
|
146
|
+
[[0x0f, 9], [0x0a, 8], [0x0d, 8], [0x0c, 7]],
|
|
147
|
+
// totalCoeff=11
|
|
148
|
+
[[0x0b, 9], [0x0e, 9], [0x09, 8], [0x0c, 8]],
|
|
149
|
+
// totalCoeff=12
|
|
150
|
+
[[0x08, 9], [0x0a, 9], [0x0d, 9], [0x08, 8]],
|
|
151
|
+
// totalCoeff=13
|
|
152
|
+
[[0x0d, 10], [0x07, 9], [0x09, 9], [0x0c, 9]],
|
|
153
|
+
// totalCoeff=14
|
|
154
|
+
[[0x09, 10], [0x0c, 10], [0x0b, 10], [0x0a, 10]],
|
|
155
|
+
// totalCoeff=15
|
|
156
|
+
[[0x05, 10], [0x08, 10], [0x07, 10], [0x06, 10]],
|
|
157
|
+
// totalCoeff=16
|
|
158
|
+
[[0x01, 10], [0x04, 10], [0x03, 10], [0x02, 10]],
|
|
159
|
+
],
|
|
160
|
+
|
|
161
|
+
// ── nC >= 8 (table 3) ── fixed-length 6-bit codes
|
|
162
|
+
[
|
|
163
|
+
// totalCoeff=1
|
|
164
|
+
[[0x00, 6], [0x01, 6], [0, 0], [0, 0]],
|
|
165
|
+
// totalCoeff=2
|
|
166
|
+
[[0x04, 6], [0x05, 6], [0x06, 6], [0, 0]],
|
|
167
|
+
// totalCoeff=3
|
|
168
|
+
[[0x08, 6], [0x09, 6], [0x0a, 6], [0x0b, 6]],
|
|
169
|
+
// totalCoeff=4
|
|
170
|
+
[[0x0c, 6], [0x0d, 6], [0x0e, 6], [0x0f, 6]],
|
|
171
|
+
// totalCoeff=5
|
|
172
|
+
[[0x10, 6], [0x11, 6], [0x12, 6], [0x13, 6]],
|
|
173
|
+
// totalCoeff=6
|
|
174
|
+
[[0x14, 6], [0x15, 6], [0x16, 6], [0x17, 6]],
|
|
175
|
+
// totalCoeff=7
|
|
176
|
+
[[0x18, 6], [0x19, 6], [0x1a, 6], [0x1b, 6]],
|
|
177
|
+
// totalCoeff=8
|
|
178
|
+
[[0x1c, 6], [0x1d, 6], [0x1e, 6], [0x1f, 6]],
|
|
179
|
+
// totalCoeff=9
|
|
180
|
+
[[0x20, 6], [0x21, 6], [0x22, 6], [0x23, 6]],
|
|
181
|
+
// totalCoeff=10
|
|
182
|
+
[[0x24, 6], [0x25, 6], [0x26, 6], [0x27, 6]],
|
|
183
|
+
// totalCoeff=11
|
|
184
|
+
[[0x28, 6], [0x29, 6], [0x2a, 6], [0x2b, 6]],
|
|
185
|
+
// totalCoeff=12
|
|
186
|
+
[[0x2c, 6], [0x2d, 6], [0x2e, 6], [0x2f, 6]],
|
|
187
|
+
// totalCoeff=13
|
|
188
|
+
[[0x30, 6], [0x31, 6], [0x32, 6], [0x33, 6]],
|
|
189
|
+
// totalCoeff=14
|
|
190
|
+
[[0x34, 6], [0x35, 6], [0x36, 6], [0x37, 6]],
|
|
191
|
+
// totalCoeff=15
|
|
192
|
+
[[0x38, 6], [0x39, 6], [0x3a, 6], [0x3b, 6]],
|
|
193
|
+
// totalCoeff=16
|
|
194
|
+
[[0x3c, 6], [0x3d, 6], [0x3e, 6], [0x3f, 6]],
|
|
195
|
+
],
|
|
196
|
+
|
|
197
|
+
// ── nC = -1: Chroma DC 2x2 (4:2:0) (table 4) ── maxCoeff=4
|
|
198
|
+
[
|
|
199
|
+
// totalCoeff=1
|
|
200
|
+
[[0x07, 6], [0x01, 1], [0, 0], [0, 0]],
|
|
201
|
+
// totalCoeff=2
|
|
202
|
+
[[0x04, 6], [0x06, 6], [0x01, 3], [0, 0]],
|
|
203
|
+
// totalCoeff=3
|
|
204
|
+
[[0x03, 6], [0x03, 7], [0x02, 7], [0x05, 6]],
|
|
205
|
+
// totalCoeff=4
|
|
206
|
+
[[0x02, 6], [0x03, 8], [0x02, 8], [0x00, 7]],
|
|
207
|
+
],
|
|
208
|
+
|
|
209
|
+
// ── nC = -2: Chroma DC 2x4 (4:2:2) (table 5) ── maxCoeff=8
|
|
210
|
+
[
|
|
211
|
+
// totalCoeff=1
|
|
212
|
+
[[0x0f, 7], [0x01, 2], [0, 0], [0, 0]],
|
|
213
|
+
// totalCoeff=2
|
|
214
|
+
[[0x0e, 7], [0x0d, 7], [0x01, 3], [0, 0]],
|
|
215
|
+
// totalCoeff=3
|
|
216
|
+
[[0x07, 9], [0x0c, 7], [0x0b, 7], [0x01, 5]],
|
|
217
|
+
// totalCoeff=4
|
|
218
|
+
[[0x06, 9], [0x05, 9], [0x0a, 7], [0x01, 6]],
|
|
219
|
+
// totalCoeff=5
|
|
220
|
+
[[0x07, 10], [0x06, 10], [0x04, 9], [0x09, 7]],
|
|
221
|
+
// totalCoeff=6
|
|
222
|
+
[[0x07, 11], [0x06, 11], [0x05, 10], [0x08, 7]],
|
|
223
|
+
// totalCoeff=7
|
|
224
|
+
[[0x07, 12], [0x06, 12], [0x05, 11], [0x04, 10]],
|
|
225
|
+
// totalCoeff=8
|
|
226
|
+
[[0x07, 13], [0x05, 12], [0x04, 12], [0x04, 11]],
|
|
227
|
+
],
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Maps nC (predicted number of non-zero coefficients) to table index.
|
|
232
|
+
* nC < 0: use -1 → index 4 (ChromaDC 2x2) or -2 → index 5 (ChromaDC 2x4)
|
|
233
|
+
* nC 0-1: index 0
|
|
234
|
+
* nC 2-3: index 1
|
|
235
|
+
* nC 4-7: index 2
|
|
236
|
+
* nC >= 8: index 3
|
|
237
|
+
*/
|
|
238
|
+
export function nCtoTableIndex(nC) {
|
|
239
|
+
if (nC < 0) return nC === -1 ? 4 : 5;
|
|
240
|
+
if (nC <= 1) return 0;
|
|
241
|
+
if (nC <= 3) return 1;
|
|
242
|
+
if (nC <= 7) return 2;
|
|
243
|
+
return 3;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
247
|
+
// total_zeros VLC tables (Table 9-7 in the spec)
|
|
248
|
+
// totalZerosTable[totalCoeff-1][totalZeros] = [bits, length]
|
|
249
|
+
// For 4x4 blocks, TotalCoeff = 1..15
|
|
250
|
+
// totalZeros range: 0..(16-TotalCoeff)
|
|
251
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
252
|
+
|
|
253
|
+
export const totalZerosTable = [
|
|
254
|
+
// totalCoeff=1: totalZeros 0..15
|
|
255
|
+
[
|
|
256
|
+
[0x01, 1], [0x03, 3], [0x02, 3], [0x03, 4],
|
|
257
|
+
[0x02, 4], [0x03, 5], [0x02, 5], [0x03, 6],
|
|
258
|
+
[0x02, 6], [0x03, 7], [0x02, 7], [0x03, 8],
|
|
259
|
+
[0x02, 8], [0x03, 9], [0x02, 9], [0x01, 9],
|
|
260
|
+
],
|
|
261
|
+
// totalCoeff=2: totalZeros 0..14
|
|
262
|
+
[
|
|
263
|
+
[0x07, 3], [0x06, 3], [0x05, 3], [0x04, 3],
|
|
264
|
+
[0x03, 3], [0x05, 4], [0x04, 4], [0x03, 4],
|
|
265
|
+
[0x02, 4], [0x03, 5], [0x02, 5], [0x03, 6],
|
|
266
|
+
[0x02, 6], [0x01, 6], [0x00, 6],
|
|
267
|
+
],
|
|
268
|
+
// totalCoeff=3: totalZeros 0..13
|
|
269
|
+
[
|
|
270
|
+
[0x05, 4], [0x07, 3], [0x06, 3], [0x05, 3],
|
|
271
|
+
[0x04, 4], [0x03, 4], [0x04, 3], [0x03, 3],
|
|
272
|
+
[0x02, 4], [0x03, 5], [0x02, 5], [0x01, 6],
|
|
273
|
+
[0x01, 5], [0x00, 6],
|
|
274
|
+
],
|
|
275
|
+
// totalCoeff=4: totalZeros 0..12
|
|
276
|
+
[
|
|
277
|
+
[0x03, 5], [0x07, 3], [0x05, 4], [0x04, 4],
|
|
278
|
+
[0x06, 3], [0x05, 3], [0x04, 3], [0x03, 4],
|
|
279
|
+
[0x03, 3], [0x02, 4], [0x02, 5], [0x01, 5],
|
|
280
|
+
[0x00, 5],
|
|
281
|
+
],
|
|
282
|
+
// totalCoeff=5: totalZeros 0..11
|
|
283
|
+
[
|
|
284
|
+
[0x05, 4], [0x04, 4], [0x03, 4], [0x07, 3],
|
|
285
|
+
[0x06, 3], [0x05, 3], [0x04, 3], [0x03, 3],
|
|
286
|
+
[0x02, 4], [0x01, 5], [0x01, 4], [0x00, 5],
|
|
287
|
+
],
|
|
288
|
+
// totalCoeff=6: totalZeros 0..10
|
|
289
|
+
[
|
|
290
|
+
[0x01, 6], [0x01, 5], [0x07, 3], [0x06, 3],
|
|
291
|
+
[0x05, 3], [0x04, 3], [0x03, 3], [0x02, 3],
|
|
292
|
+
[0x01, 4], [0x01, 3], [0x00, 6],
|
|
293
|
+
],
|
|
294
|
+
// totalCoeff=7: totalZeros 0..9
|
|
295
|
+
[
|
|
296
|
+
[0x01, 6], [0x01, 5], [0x05, 3], [0x04, 3],
|
|
297
|
+
[0x03, 3], [0x03, 2], [0x02, 3], [0x01, 4],
|
|
298
|
+
[0x01, 3], [0x00, 6],
|
|
299
|
+
],
|
|
300
|
+
// totalCoeff=8: totalZeros 0..8
|
|
301
|
+
[
|
|
302
|
+
[0x01, 6], [0x01, 4], [0x01, 5], [0x03, 3],
|
|
303
|
+
[0x03, 2], [0x02, 2], [0x02, 3], [0x01, 3],
|
|
304
|
+
[0x00, 6],
|
|
305
|
+
],
|
|
306
|
+
// totalCoeff=9: totalZeros 0..7
|
|
307
|
+
[
|
|
308
|
+
[0x01, 6], [0x00, 6], [0x01, 4], [0x03, 2],
|
|
309
|
+
[0x02, 2], [0x01, 3], [0x01, 2], [0x01, 5],
|
|
310
|
+
],
|
|
311
|
+
// totalCoeff=10: totalZeros 0..6
|
|
312
|
+
[
|
|
313
|
+
[0x01, 5], [0x00, 5], [0x01, 3], [0x03, 2],
|
|
314
|
+
[0x02, 2], [0x01, 2], [0x01, 4],
|
|
315
|
+
],
|
|
316
|
+
// totalCoeff=11: totalZeros 0..5
|
|
317
|
+
[
|
|
318
|
+
[0x00, 4], [0x01, 4], [0x01, 3], [0x02, 3],
|
|
319
|
+
[0x01, 1], [0x03, 3],
|
|
320
|
+
],
|
|
321
|
+
// totalCoeff=12: totalZeros 0..4
|
|
322
|
+
[
|
|
323
|
+
[0x00, 4], [0x01, 4], [0x01, 2], [0x01, 1],
|
|
324
|
+
[0x01, 3],
|
|
325
|
+
],
|
|
326
|
+
// totalCoeff=13: totalZeros 0..3
|
|
327
|
+
[
|
|
328
|
+
[0x00, 3], [0x01, 3], [0x01, 1], [0x01, 2],
|
|
329
|
+
],
|
|
330
|
+
// totalCoeff=14: totalZeros 0..2
|
|
331
|
+
[
|
|
332
|
+
[0x00, 2], [0x01, 2], [0x01, 1],
|
|
333
|
+
],
|
|
334
|
+
// totalCoeff=15: totalZeros 0..1
|
|
335
|
+
[
|
|
336
|
+
[0x00, 1], [0x01, 1],
|
|
337
|
+
],
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
341
|
+
// total_zeros for Chroma DC 2x2 (4:2:0) — Table 9-8 in the spec
|
|
342
|
+
// totalZerosChromaDC2x2[totalCoeff-1][totalZeros] = [bits, length]
|
|
343
|
+
// maxCoeff = 4, so totalCoeff = 1..3
|
|
344
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
345
|
+
|
|
346
|
+
export const totalZerosChromaDC2x2 = [
|
|
347
|
+
// totalCoeff=1: totalZeros 0..3
|
|
348
|
+
[
|
|
349
|
+
[0x01, 1], [0x01, 2], [0x01, 3], [0x00, 3],
|
|
350
|
+
],
|
|
351
|
+
// totalCoeff=2: totalZeros 0..2
|
|
352
|
+
[
|
|
353
|
+
[0x01, 1], [0x01, 2], [0x00, 2],
|
|
354
|
+
],
|
|
355
|
+
// totalCoeff=3: totalZeros 0..1
|
|
356
|
+
[
|
|
357
|
+
[0x01, 1], [0x00, 1],
|
|
358
|
+
],
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
362
|
+
// total_zeros for Chroma DC 2x4 (4:2:2) — Table 9-9 in the spec
|
|
363
|
+
// totalZerosChromaDC2x4[totalCoeff-1][totalZeros] = [bits, length]
|
|
364
|
+
// maxCoeff = 8, so totalCoeff = 1..7
|
|
365
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
366
|
+
|
|
367
|
+
export const totalZerosChromaDC2x4 = [
|
|
368
|
+
// totalCoeff=1: totalZeros 0..7
|
|
369
|
+
[
|
|
370
|
+
[0x01, 1], [0x02, 3], [0x03, 3], [0x02, 4],
|
|
371
|
+
[0x03, 4], [0x01, 4], [0x01, 5], [0x00, 5],
|
|
372
|
+
],
|
|
373
|
+
// totalCoeff=2: totalZeros 0..6
|
|
374
|
+
[
|
|
375
|
+
[0x00, 3], [0x01, 2], [0x01, 3], [0x04, 3],
|
|
376
|
+
[0x05, 3], [0x06, 3], [0x07, 3],
|
|
377
|
+
],
|
|
378
|
+
// totalCoeff=3: totalZeros 0..5
|
|
379
|
+
[
|
|
380
|
+
[0x00, 3], [0x01, 3], [0x01, 2], [0x02, 2],
|
|
381
|
+
[0x06, 3], [0x07, 3],
|
|
382
|
+
],
|
|
383
|
+
// totalCoeff=4: totalZeros 0..4
|
|
384
|
+
[
|
|
385
|
+
[0x06, 3], [0x00, 2], [0x01, 2], [0x02, 2],
|
|
386
|
+
[0x07, 3],
|
|
387
|
+
],
|
|
388
|
+
// totalCoeff=5: totalZeros 0..3
|
|
389
|
+
[
|
|
390
|
+
[0x00, 2], [0x01, 2], [0x02, 2], [0x03, 2],
|
|
391
|
+
],
|
|
392
|
+
// totalCoeff=6: totalZeros 0..2
|
|
393
|
+
[
|
|
394
|
+
[0x00, 2], [0x01, 2], [0x01, 1],
|
|
395
|
+
],
|
|
396
|
+
// totalCoeff=7: totalZeros 0..1
|
|
397
|
+
[
|
|
398
|
+
[0x00, 1], [0x01, 1],
|
|
399
|
+
],
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
403
|
+
// run_before VLC tables — Table 9-10 in the spec
|
|
404
|
+
// runBeforeTable[min(zerosLeft,7)-1][runBefore] = [bits, length]
|
|
405
|
+
//
|
|
406
|
+
// zerosLeft=1..6 have individual tables
|
|
407
|
+
// zerosLeft>=7 shares one table (index 6)
|
|
408
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
409
|
+
|
|
410
|
+
export const runBeforeTable = [
|
|
411
|
+
// zerosLeft=1: runBefore 0..1
|
|
412
|
+
[
|
|
413
|
+
[0x01, 1], [0x00, 1],
|
|
414
|
+
],
|
|
415
|
+
// zerosLeft=2: runBefore 0..2
|
|
416
|
+
[
|
|
417
|
+
[0x01, 1], [0x01, 2], [0x00, 2],
|
|
418
|
+
],
|
|
419
|
+
// zerosLeft=3: runBefore 0..3
|
|
420
|
+
[
|
|
421
|
+
[0x03, 2], [0x02, 2], [0x01, 2], [0x00, 2],
|
|
422
|
+
],
|
|
423
|
+
// zerosLeft=4: runBefore 0..4
|
|
424
|
+
[
|
|
425
|
+
[0x03, 2], [0x02, 2], [0x01, 2], [0x01, 3], [0x00, 3],
|
|
426
|
+
],
|
|
427
|
+
// zerosLeft=5: runBefore 0..5
|
|
428
|
+
[
|
|
429
|
+
[0x03, 2], [0x02, 2], [0x03, 3], [0x02, 3], [0x01, 3], [0x00, 3],
|
|
430
|
+
],
|
|
431
|
+
// zerosLeft=6: runBefore 0..6
|
|
432
|
+
[
|
|
433
|
+
[0x03, 2], [0x00, 3], [0x01, 3], [0x03, 3], [0x02, 3], [0x05, 3], [0x04, 3],
|
|
434
|
+
],
|
|
435
|
+
// zerosLeft>=7: runBefore 0..14
|
|
436
|
+
[
|
|
437
|
+
[0x07, 3], [0x06, 3], [0x05, 3], [0x04, 3],
|
|
438
|
+
[0x03, 3], [0x02, 3], [0x01, 3], [0x01, 4],
|
|
439
|
+
[0x01, 5], [0x01, 6], [0x01, 7], [0x01, 8],
|
|
440
|
+
[0x01, 9], [0x01, 10], [0x01, 11],
|
|
441
|
+
],
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
445
|
+
// Level encoding (coefficient magnitude/sign)
|
|
446
|
+
//
|
|
447
|
+
// The H.264 CAVLC level encoding uses:
|
|
448
|
+
// 1) level_prefix: unary code (N zeros followed by a 1)
|
|
449
|
+
// 2) level_suffix: fixed-length code of length suffixLength
|
|
450
|
+
//
|
|
451
|
+
// The suffixLength starts at 0 (or 1 if totalCoeff>10 && trailingOnes<3)
|
|
452
|
+
// and increases as larger levels are encountered.
|
|
453
|
+
//
|
|
454
|
+
// This function encodes a single coefficient level and returns
|
|
455
|
+
// [bits, length, newSuffixLength].
|
|
456
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Thresholds for incrementing suffixLength.
|
|
460
|
+
* After encoding a level, if |level| > nextSuffix[suffixLength],
|
|
461
|
+
* increment suffixLength.
|
|
462
|
+
* From x264: {0, 3, 6, 12, 24, 48, 0xffff}
|
|
463
|
+
*/
|
|
464
|
+
const SUFFIX_THRESHOLD = [0, 3, 6, 12, 24, 48, 0x7fffffff];
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Encode a single coefficient level value using CAVLC level VLC.
|
|
468
|
+
*
|
|
469
|
+
* Algorithm from ITU-T H.264 Section 9.2.2 and verified against
|
|
470
|
+
* x264 (common/vlc.c x264_level_token / encoder/cavlc.c cavlc_block_residual_escape).
|
|
471
|
+
*
|
|
472
|
+
* @param {number} level - Signed coefficient level (non-zero)
|
|
473
|
+
* @param {number} suffixLength - Current suffix length (0-6)
|
|
474
|
+
* @param {boolean} firstAfterTrailing - True if this is the first level after
|
|
475
|
+
* trailing ones and trailingOnes < 3. The spec says to subtract 1 from the
|
|
476
|
+
* magnitude (since we know it must be > 1 in this case).
|
|
477
|
+
* @returns {{ bits: number, length: number, suffixLength: number }}
|
|
478
|
+
*/
|
|
479
|
+
export function encodeLevel(level, suffixLength, firstAfterTrailing) {
|
|
480
|
+
const sign = level < 0 ? 1 : 0;
|
|
481
|
+
let absLevel = Math.abs(level);
|
|
482
|
+
|
|
483
|
+
// level_code = 2*|level| - 2 + sign
|
|
484
|
+
// If firstAfterTrailing, the decoded level is offset by 1
|
|
485
|
+
// (since |level| > 1 is guaranteed), so we adjust:
|
|
486
|
+
let levelCode = (absLevel << 1) - 2 + sign;
|
|
487
|
+
if (firstAfterTrailing) {
|
|
488
|
+
levelCode -= 2;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let bits, length;
|
|
492
|
+
|
|
493
|
+
const prefix = levelCode >> suffixLength;
|
|
494
|
+
|
|
495
|
+
if (prefix < 14) {
|
|
496
|
+
// Normal case: prefix zeros + 1 + suffix bits
|
|
497
|
+
length = prefix + 1 + suffixLength;
|
|
498
|
+
bits = (1 << suffixLength) | (levelCode & ((1 << suffixLength) - 1));
|
|
499
|
+
} else if (suffixLength === 0 && prefix === 14) {
|
|
500
|
+
// Special case: suffixLength=0, prefix=14 uses 4-bit suffix
|
|
501
|
+
length = 15 + 4; // 19 bits total (14 zeros + 1 + 4 suffix bits)
|
|
502
|
+
bits = (1 << 4) | (levelCode - 14);
|
|
503
|
+
} else if (prefix === 14) {
|
|
504
|
+
// suffixLength > 0, prefix=14: normal encoding still applies
|
|
505
|
+
length = 14 + 1 + suffixLength;
|
|
506
|
+
bits = (1 << suffixLength) | (levelCode & ((1 << suffixLength) - 1));
|
|
507
|
+
} else {
|
|
508
|
+
// prefix >= 15: escape code (High Profile level codes)
|
|
509
|
+
let escapeLevelCode = levelCode;
|
|
510
|
+
escapeLevelCode -= 15 << suffixLength;
|
|
511
|
+
if (suffixLength === 0) {
|
|
512
|
+
escapeLevelCode -= 15;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let levelPrefix = 15;
|
|
516
|
+
// For very large values, extend the prefix (High Profile)
|
|
517
|
+
while (escapeLevelCode >= (1 << (levelPrefix - 3))) {
|
|
518
|
+
escapeLevelCode -= 1 << (levelPrefix - 3);
|
|
519
|
+
levelPrefix++;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// prefix unary code (levelPrefix zeros + 1) + (levelPrefix-3) suffix bits
|
|
523
|
+
length = levelPrefix + 1 + (levelPrefix - 3);
|
|
524
|
+
bits = (1 << (levelPrefix - 3)) | (escapeLevelCode & ((1 << (levelPrefix - 3)) - 1));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Update suffixLength
|
|
528
|
+
let newSuffixLength = suffixLength;
|
|
529
|
+
if (newSuffixLength === 0) {
|
|
530
|
+
newSuffixLength = 1;
|
|
531
|
+
}
|
|
532
|
+
if (absLevel > SUFFIX_THRESHOLD[newSuffixLength]) {
|
|
533
|
+
newSuffixLength++;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return { bits, length, suffixLength: newSuffixLength };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Encode the full set of non-trailing-one levels for a residual block.
|
|
541
|
+
*
|
|
542
|
+
* @param {number[]} levels - Array of signed level values (non-trailing, in reverse scan order)
|
|
543
|
+
* @param {number} trailingOnes - Number of trailing ones (0-3)
|
|
544
|
+
* @param {number} totalCoeff - Total number of non-zero coefficients
|
|
545
|
+
* @returns {{ bits: number[], lengths: number[] }} - Arrays of VLC codewords
|
|
546
|
+
*/
|
|
547
|
+
export function encodeLevels(levels, trailingOnes, totalCoeff) {
|
|
548
|
+
const bits = [];
|
|
549
|
+
const lengths = [];
|
|
550
|
+
|
|
551
|
+
// Initial suffix length
|
|
552
|
+
let suffixLength = (totalCoeff > 10 && trailingOnes < 3) ? 1 : 0;
|
|
553
|
+
|
|
554
|
+
for (let i = 0; i < levels.length; i++) {
|
|
555
|
+
const firstAfterTrailing = (i === 0 && trailingOnes < 3);
|
|
556
|
+
const result = encodeLevel(levels[i], suffixLength, firstAfterTrailing);
|
|
557
|
+
bits.push(result.bits);
|
|
558
|
+
lengths.push(result.length);
|
|
559
|
+
suffixLength = result.suffixLength;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return { bits, lengths };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
566
|
+
// Convenience: encode a full CAVLC residual block
|
|
567
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Write coeff_token to a bitstream writer.
|
|
571
|
+
*
|
|
572
|
+
* @param {number} totalCoeff - Total non-zero coefficients (0-16)
|
|
573
|
+
* @param {number} trailingOnes - Number of trailing +/-1 coefficients (0-3)
|
|
574
|
+
* @param {number} nC - Predicted number of non-zero coefficients
|
|
575
|
+
* @returns {[number, number]} [bits, length] for the coeff_token VLC
|
|
576
|
+
*/
|
|
577
|
+
export function getCoeffToken(totalCoeff, trailingOnes, nC) {
|
|
578
|
+
const tableIdx = nCtoTableIndex(nC);
|
|
579
|
+
if (totalCoeff === 0) {
|
|
580
|
+
return coeff0Token[tableIdx];
|
|
581
|
+
}
|
|
582
|
+
return coeffTokenTable[tableIdx][totalCoeff - 1][trailingOnes];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Get total_zeros VLC for a 4x4 block.
|
|
587
|
+
*
|
|
588
|
+
* @param {number} totalCoeff - 1..15
|
|
589
|
+
* @param {number} totalZeros - 0..(16-totalCoeff)
|
|
590
|
+
* @returns {[number, number]} [bits, length]
|
|
591
|
+
*/
|
|
592
|
+
export function getTotalZeros(totalCoeff, totalZeros) {
|
|
593
|
+
return totalZerosTable[totalCoeff - 1][totalZeros];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Get total_zeros VLC for chroma DC 2x2 block.
|
|
598
|
+
*
|
|
599
|
+
* @param {number} totalCoeff - 1..3
|
|
600
|
+
* @param {number} totalZeros - 0..(4-totalCoeff)
|
|
601
|
+
* @returns {[number, number]} [bits, length]
|
|
602
|
+
*/
|
|
603
|
+
export function getTotalZerosChromaDC(totalCoeff, totalZeros) {
|
|
604
|
+
return totalZerosChromaDC2x2[totalCoeff - 1][totalZeros];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Get total_zeros VLC for chroma DC 2x4 block (4:2:2).
|
|
609
|
+
*
|
|
610
|
+
* @param {number} totalCoeff - 1..7
|
|
611
|
+
* @param {number} totalZeros - 0..(8-totalCoeff)
|
|
612
|
+
* @returns {[number, number]} [bits, length]
|
|
613
|
+
*/
|
|
614
|
+
export function getTotalZerosChromaDC422(totalCoeff, totalZeros) {
|
|
615
|
+
return totalZerosChromaDC2x4[totalCoeff - 1][totalZeros];
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Get run_before VLC.
|
|
620
|
+
*
|
|
621
|
+
* @param {number} zerosLeft - Remaining zeros to distribute (>= 1)
|
|
622
|
+
* @param {number} runBefore - Run of zeros before this coefficient
|
|
623
|
+
* @returns {[number, number]} [bits, length]
|
|
624
|
+
*/
|
|
625
|
+
export function getRunBefore(zerosLeft, runBefore) {
|
|
626
|
+
const idx = Math.min(zerosLeft, 7) - 1;
|
|
627
|
+
return runBeforeTable[idx][runBefore];
|
|
628
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { forwardDCT4x4, forwardHadamard4x4, forwardHadamard2x2, quantize4x4, clip255 } from './h264-transform.js';
|
|
15
15
|
import { scanOrder4x4 } from './h264-tables.js';
|
|
16
|
+
import { getCoeffToken, getTotalZeros, getTotalZerosChromaDC, getRunBefore, encodeLevels } from './h264-cavlc-tables.js';
|
|
16
17
|
|
|
17
18
|
// ── Bitstream Writer ──────────────────────────────────────
|
|
18
19
|
|
|
@@ -382,6 +383,9 @@ export class H264Encoder {
|
|
|
382
383
|
const mbType = 1 + predMode + cbpChroma * 4 + (cbpLuma > 0 ? 12 : 0);
|
|
383
384
|
bs.writeUE(mbType);
|
|
384
385
|
|
|
386
|
+
// intra_chroma_pred_mode = 0 (DC) — required for ALL intra MBs
|
|
387
|
+
bs.writeUE(0);
|
|
388
|
+
|
|
385
389
|
// mb_qp_delta = 0 (first MB uses slice QP)
|
|
386
390
|
bs.writeSE(0);
|
|
387
391
|
|
|
@@ -400,178 +404,99 @@ export class H264Encoder {
|
|
|
400
404
|
// (The chroma prediction handles the base values)
|
|
401
405
|
}
|
|
402
406
|
|
|
403
|
-
// ── CAVLC Block Encoding
|
|
407
|
+
// ── CAVLC Block Encoding (using spec-correct tables) ────
|
|
404
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Encode a residual block using CAVLC with the correct VLC tables
|
|
411
|
+
* from the H.264 spec (Tables 9-5 through 9-10).
|
|
412
|
+
*
|
|
413
|
+
* @param {BitstreamWriter} bs - Output bitstream
|
|
414
|
+
* @param {Int32Array} coeffs - Quantized coefficients in scan order
|
|
415
|
+
* @param {number} maxCoeff - Maximum coefficients (16 for 4x4, 15 for AC)
|
|
416
|
+
* @param {number} nC - Predicted number of non-zero coefficients
|
|
417
|
+
*/
|
|
405
418
|
_encodeCavlcBlock(bs, coeffs, maxCoeff, nC) {
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
419
|
+
// Step 1: Analyze coefficients in reverse scan order
|
|
420
|
+
// Find non-zero coefficients and count trailing ones
|
|
421
|
+
const nonZeroValues = []; // level values in reverse scan order
|
|
422
|
+
const nonZeroPositions = []; // scan positions
|
|
410
423
|
|
|
411
|
-
// Scan in reverse order to find trailing ones
|
|
412
424
|
for (let i = maxCoeff - 1; i >= 0; i--) {
|
|
413
425
|
if (coeffs[i] !== 0) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
trailingOnes++;
|
|
417
|
-
}
|
|
418
|
-
levels.push(coeffs[i]);
|
|
419
|
-
} else if (levels.length > 0) {
|
|
420
|
-
levels.push(0); // placeholder for run counting
|
|
426
|
+
nonZeroValues.push(coeffs[i]);
|
|
427
|
+
nonZeroPositions.push(i);
|
|
421
428
|
}
|
|
422
429
|
}
|
|
423
430
|
|
|
424
|
-
|
|
425
|
-
const tableIdx = nC < 2 ? 0 : nC < 4 ? 1 : nC < 8 ? 2 : 3;
|
|
431
|
+
const totalCoeff = nonZeroValues.length;
|
|
426
432
|
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return;
|
|
433
|
+
// Count trailing ones (T1s): consecutive +/-1 at the END of the non-zero list
|
|
434
|
+
// In reverse scan order, these are at the BEGINNING of nonZeroValues
|
|
435
|
+
let trailingOnes = 0;
|
|
436
|
+
for (let i = 0; i < Math.min(totalCoeff, 3); i++) {
|
|
437
|
+
if (Math.abs(nonZeroValues[i]) === 1) trailingOnes++;
|
|
438
|
+
else break;
|
|
434
439
|
}
|
|
435
440
|
|
|
436
|
-
//
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
const table = CAVLC_COEFF_TOKEN[Math.min(tableIdx, 1)];
|
|
440
|
-
if (table[tc] && table[tc][to]) {
|
|
441
|
-
const [code, len] = table[tc][to];
|
|
442
|
-
bs.writeBits(code, len);
|
|
443
|
-
} else {
|
|
444
|
-
// Fallback: write as zero block
|
|
445
|
-
const [code, len] = table[0][0];
|
|
446
|
-
bs.writeBits(code, len);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
441
|
+
// Step 2: Write coeff_token
|
|
442
|
+
const [ctBits, ctLen] = getCoeffToken(totalCoeff, trailingOnes, nC);
|
|
443
|
+
bs.writeBits(ctBits, ctLen);
|
|
449
444
|
|
|
450
|
-
|
|
451
|
-
const nonZeroLevels = [];
|
|
452
|
-
for (let i = maxCoeff - 1; i >= 0; i--) {
|
|
453
|
-
if (coeffs[i] !== 0) nonZeroLevels.push(coeffs[i]);
|
|
454
|
-
}
|
|
445
|
+
if (totalCoeff === 0) return;
|
|
455
446
|
|
|
447
|
+
// Step 3: Write trailing ones sign flags (1 bit each, 0=positive, 1=negative)
|
|
456
448
|
for (let i = 0; i < trailingOnes; i++) {
|
|
457
|
-
bs.writeBit(
|
|
449
|
+
bs.writeBit(nonZeroValues[i] < 0 ? 1 : 0);
|
|
458
450
|
}
|
|
459
451
|
|
|
460
|
-
// Write remaining levels (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
// level_prefix: unary code
|
|
472
|
-
let levelCode = (absLevel - 1) * 2 + sign;
|
|
473
|
-
if (i === trailingOnes && trailingOnes < 3) {
|
|
474
|
-
levelCode = absLevel * 2 + sign;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const prefix = levelCode >> suffixLength;
|
|
478
|
-
// Write prefix as unary
|
|
479
|
-
for (let j = 0; j < Math.min(prefix, 15); j++) bs.writeBit(0);
|
|
480
|
-
bs.writeBit(1);
|
|
481
|
-
|
|
482
|
-
// Write suffix
|
|
483
|
-
if (suffixLength > 0 || prefix >= 14) {
|
|
484
|
-
const suffBits = prefix >= 15 ? 12 : suffixLength;
|
|
485
|
-
if (suffBits > 0) {
|
|
486
|
-
bs.writeBits(levelCode & ((1 << suffBits) - 1), suffBits);
|
|
487
|
-
}
|
|
452
|
+
// Step 4: Write remaining levels (non-trailing-ones, still in reverse scan order)
|
|
453
|
+
if (totalCoeff > trailingOnes) {
|
|
454
|
+
const remainingLevels = nonZeroValues.slice(trailingOnes);
|
|
455
|
+
const { bits: levelBits, lengths: levelLens } = encodeLevels(
|
|
456
|
+
remainingLevels, trailingOnes, totalCoeff
|
|
457
|
+
);
|
|
458
|
+
for (let i = 0; i < levelBits.length; i++) {
|
|
459
|
+
// Write prefix (zeros + 1)
|
|
460
|
+
const prefix = levelLens[i] - (levelLens[i] > 0 ? 0 : 0);
|
|
461
|
+
// encodeLevels returns {bits, length} — write directly
|
|
462
|
+
bs.writeBits(levelBits[i], levelLens[i]);
|
|
488
463
|
}
|
|
489
|
-
|
|
490
|
-
// Update suffix length
|
|
491
|
-
if (suffixLength === 0) suffixLength = 1;
|
|
492
|
-
if (absLevel > (3 << (suffixLength - 1))) suffixLength++;
|
|
493
464
|
}
|
|
494
465
|
|
|
495
|
-
// Write total_zeros
|
|
466
|
+
// Step 5: Write total_zeros (only if totalCoeff < maxCoeff)
|
|
496
467
|
if (totalCoeff < maxCoeff) {
|
|
497
|
-
|
|
498
|
-
let
|
|
468
|
+
// Count total zeros before (and between) the non-zero coefficients
|
|
469
|
+
let lastNonZeroPos = 0;
|
|
499
470
|
for (let i = maxCoeff - 1; i >= 0; i--) {
|
|
500
|
-
if (coeffs[i] !== 0) {
|
|
471
|
+
if (coeffs[i] !== 0) { lastNonZeroPos = i; break; }
|
|
501
472
|
}
|
|
502
|
-
|
|
473
|
+
let totalZeros = 0;
|
|
474
|
+
for (let i = 0; i <= lastNonZeroPos; i++) {
|
|
503
475
|
if (coeffs[i] === 0) totalZeros++;
|
|
504
476
|
}
|
|
505
477
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
this._writeTotalZeros(bs, totalZeros, totalCoeff, maxCoeff);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Write run_before for each coefficient (except the last one)
|
|
512
|
-
let zerosLeft = 0;
|
|
513
|
-
for (let i = maxCoeff - 1; i >= 0; i--) {
|
|
514
|
-
if (coeffs[i] === 0) zerosLeft++;
|
|
515
|
-
}
|
|
516
|
-
// Adjust to only count zeros before the last non-zero
|
|
517
|
-
zerosLeft = 0;
|
|
518
|
-
let found = 0;
|
|
519
|
-
for (let i = 0; i < maxCoeff && found < totalCoeff; i++) {
|
|
520
|
-
if (coeffs[i] !== 0) {
|
|
521
|
-
found++;
|
|
522
|
-
} else if (found < totalCoeff) {
|
|
523
|
-
zerosLeft++;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
478
|
+
const [tzBits, tzLen] = getTotalZeros(totalCoeff, totalZeros);
|
|
479
|
+
bs.writeBits(tzBits, tzLen);
|
|
526
480
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
// Count
|
|
481
|
+
// Step 6: Write run_before for each coefficient (reverse scan order)
|
|
482
|
+
// except the last one (its position is implied)
|
|
483
|
+
let zerosLeft = totalZeros;
|
|
484
|
+
for (let i = 0; i < totalCoeff - 1 && zerosLeft > 0; i++) {
|
|
485
|
+
const pos = nonZeroPositions[i];
|
|
486
|
+
// Count consecutive zeros before this coefficient in scan order
|
|
533
487
|
let run = 0;
|
|
534
|
-
for (let j =
|
|
488
|
+
for (let j = pos - 1; j >= 0; j--) {
|
|
535
489
|
if (coeffs[j] === 0) run++;
|
|
536
490
|
else break;
|
|
537
491
|
}
|
|
538
|
-
|
|
539
|
-
this._writeRunBefore(bs, Math.min(run, remaining), remaining);
|
|
540
|
-
remaining -= run;
|
|
541
|
-
}
|
|
542
|
-
coeffIdx++;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
492
|
+
run = Math.min(run, zerosLeft);
|
|
546
493
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (totalCoeff >= maxCoeff) return; // no zeros possible
|
|
551
|
-
const maxZeros = maxCoeff - totalCoeff;
|
|
552
|
-
if (totalZeros === 0) {
|
|
553
|
-
bs.writeBit(1);
|
|
554
|
-
} else if (totalZeros <= maxZeros) {
|
|
555
|
-
for (let i = 0; i < Math.min(totalZeros, 8); i++) bs.writeBit(0);
|
|
556
|
-
bs.writeBit(1);
|
|
557
|
-
if (totalZeros > 8) {
|
|
558
|
-
bs.writeBits(totalZeros - 8, 4);
|
|
494
|
+
const [rbBits, rbLen] = getRunBefore(zerosLeft, run);
|
|
495
|
+
bs.writeBits(rbBits, rbLen);
|
|
496
|
+
zerosLeft -= run;
|
|
559
497
|
}
|
|
560
498
|
}
|
|
561
499
|
}
|
|
562
|
-
|
|
563
|
-
_writeRunBefore(bs, run, zerosLeft) {
|
|
564
|
-
// Simplified run_before encoding
|
|
565
|
-
if (zerosLeft <= 0 || run <= 0) return;
|
|
566
|
-
if (run <= 6 && zerosLeft > 6) {
|
|
567
|
-
for (let i = 0; i < run; i++) bs.writeBit(0);
|
|
568
|
-
bs.writeBit(1);
|
|
569
|
-
} else {
|
|
570
|
-
// Truncated unary for small values
|
|
571
|
-
for (let i = 0; i < Math.min(run, 6); i++) bs.writeBit(0);
|
|
572
|
-
if (run < zerosLeft) bs.writeBit(1);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
500
|
}
|
|
576
501
|
|
|
577
502
|
export default H264Encoder;
|
package/src/hls-clip.js
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
import { parseHls, isHlsUrl, parsePlaylistText, toAbsoluteUrl } from './hls.js';
|
|
24
24
|
import { TSParser, getCodecInfo } from './parsers/mpegts.js';
|
|
25
25
|
import { createInitSegment, createFragment } from './muxers/fmp4.js';
|
|
26
|
+
import { convertFmp4ToMp4 } from './fmp4/converter.js';
|
|
27
|
+
import { parseBoxes, findBox, parseChildBoxes, createBox } from './fmp4/utils.js';
|
|
26
28
|
import { smartRender } from './codecs/smart-render.js';
|
|
27
29
|
|
|
28
30
|
// ── constants ─────────────────────────────────────────────
|
|
@@ -245,16 +247,18 @@ class HlsClipResult {
|
|
|
245
247
|
// Pre-clipped boundary segments are already in memory
|
|
246
248
|
if (seg.data) return seg.data;
|
|
247
249
|
|
|
248
|
-
// Middle segment: fetch from CDN
|
|
250
|
+
// Middle segment: fetch from CDN
|
|
249
251
|
const resp = await fetch(seg.originalUrl);
|
|
250
252
|
if (!resp.ok) throw new Error(`Segment fetch failed: ${resp.status}`);
|
|
251
|
-
const
|
|
253
|
+
const rawData = new Uint8Array(await resp.arrayBuffer());
|
|
252
254
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
+
// fMP4 segments pass through unchanged (already correct format)
|
|
256
|
+
if (seg._sourceFormat === 'fmp4') return rawData;
|
|
257
|
+
|
|
258
|
+
// TS segments: remux to fMP4
|
|
259
|
+
const parser = parseTs(rawData);
|
|
260
|
+
const audioTimescale = seg._audioTimescale || parser.audioSampleRate || 48000;
|
|
255
261
|
|
|
256
|
-
// Normalize timestamps: subtract the segment's original start PTS,
|
|
257
|
-
// then add the segment's position in the clip timeline
|
|
258
262
|
const firstVideoPts = parser.videoAccessUnits[0]?.pts ?? 0;
|
|
259
263
|
for (const au of parser.videoAccessUnits) { au.pts -= firstVideoPts; au.dts -= firstVideoPts; }
|
|
260
264
|
for (const au of parser.audioAccessUnits) { au.pts -= firstVideoPts; }
|
|
@@ -262,12 +266,7 @@ class HlsClipResult {
|
|
|
262
266
|
const videoBaseTime = Math.round(seg.timelineOffset * PTS_PER_SECOND);
|
|
263
267
|
const audioBaseTime = Math.round(seg.timelineOffset * audioTimescale);
|
|
264
268
|
|
|
265
|
-
|
|
266
|
-
parser, segmentIndex + 1,
|
|
267
|
-
videoBaseTime, audioBaseTime, audioTimescale
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
return fragment;
|
|
269
|
+
return remuxToFragment(parser, segmentIndex + 1, videoBaseTime, audioBaseTime, audioTimescale);
|
|
271
270
|
}
|
|
272
271
|
|
|
273
272
|
/**
|
|
@@ -286,6 +285,202 @@ class HlsClipResult {
|
|
|
286
285
|
}
|
|
287
286
|
}
|
|
288
287
|
|
|
288
|
+
// ── format detection ──────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
function _detectSegmentFormat(data) {
|
|
291
|
+
if (data.length < 8) return 'unknown';
|
|
292
|
+
// Check for TS sync byte
|
|
293
|
+
if (data[0] === 0x47) return 'ts';
|
|
294
|
+
for (let i = 0; i < Math.min(188, data.length); i++) {
|
|
295
|
+
if (data[i] === 0x47 && i + 188 < data.length && data[i + 188] === 0x47) return 'ts';
|
|
296
|
+
}
|
|
297
|
+
// Check for fMP4 (moof, styp, or ftyp box)
|
|
298
|
+
const type = String.fromCharCode(data[4], data[5], data[6], data[7]);
|
|
299
|
+
if (['moof', 'styp', 'ftyp', 'mdat'].includes(type)) return 'fmp4';
|
|
300
|
+
return 'unknown';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── TS variant processing ─────────────────────────────────
|
|
304
|
+
|
|
305
|
+
function _processTsVariant({ firstSegData, lastSegData, overlapping, isSingleSegment, startTime, endTime, firstSeg, lastSeg, log }) {
|
|
306
|
+
const firstParser = parseTs(firstSegData);
|
|
307
|
+
const lastParser = !isSingleSegment && lastSegData ? parseTs(lastSegData) : null;
|
|
308
|
+
|
|
309
|
+
const { sps, pps } = extractCodecInfo(firstParser);
|
|
310
|
+
if (!sps || !pps) throw new Error('Could not extract SPS/PPS from video');
|
|
311
|
+
const audioSampleRate = firstParser.audioSampleRate || 48000;
|
|
312
|
+
const audioChannels = firstParser.audioChannels || 2;
|
|
313
|
+
const hasAudio = firstParser.audioAccessUnits.length > 0;
|
|
314
|
+
const audioTimescale = audioSampleRate;
|
|
315
|
+
|
|
316
|
+
const initSegment = createInitSegment({
|
|
317
|
+
sps, pps, audioSampleRate, audioChannels, hasAudio,
|
|
318
|
+
videoTimescale: PTS_PER_SECOND, audioTimescale,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const clipSegments = [];
|
|
322
|
+
let timelineOffset = 0;
|
|
323
|
+
|
|
324
|
+
// First segment (smart-rendered)
|
|
325
|
+
const firstRelStart = startTime - firstSeg.startTime;
|
|
326
|
+
const firstRelEnd = isSingleSegment ? endTime - firstSeg.startTime : undefined;
|
|
327
|
+
const firstClipped = clipSegment(firstParser, firstRelStart, firstRelEnd);
|
|
328
|
+
if (!firstClipped) throw new Error('First segment clip produced no samples');
|
|
329
|
+
|
|
330
|
+
const firstFragment = createFragment({
|
|
331
|
+
videoSamples: firstClipped.videoSamples,
|
|
332
|
+
audioSamples: firstClipped.audioSamples,
|
|
333
|
+
sequenceNumber: 1,
|
|
334
|
+
videoTimescale: PTS_PER_SECOND, audioTimescale,
|
|
335
|
+
videoBaseTime: 0, audioBaseTime: 0, audioSampleDuration: 1024,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
clipSegments.push({
|
|
339
|
+
duration: firstClipped.duration, data: firstFragment,
|
|
340
|
+
originalUrl: null, timelineOffset: 0, isBoundary: true,
|
|
341
|
+
});
|
|
342
|
+
timelineOffset += firstClipped.duration;
|
|
343
|
+
|
|
344
|
+
// Middle segments
|
|
345
|
+
for (let i = 1; i < overlapping.length - 1; i++) {
|
|
346
|
+
clipSegments.push({
|
|
347
|
+
duration: overlapping[i].duration, data: null,
|
|
348
|
+
originalUrl: overlapping[i].url, timelineOffset, isBoundary: false,
|
|
349
|
+
_sourceFormat: 'ts', _audioTimescale: audioTimescale,
|
|
350
|
+
});
|
|
351
|
+
timelineOffset += overlapping[i].duration;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Last segment
|
|
355
|
+
if (!isSingleSegment && lastParser) {
|
|
356
|
+
const lastRelEnd = endTime - lastSeg.startTime;
|
|
357
|
+
const lastClipped = clipSegment(lastParser, undefined, lastRelEnd);
|
|
358
|
+
if (lastClipped && lastClipped.videoSamples.length > 0) {
|
|
359
|
+
const lastFragment = createFragment({
|
|
360
|
+
videoSamples: lastClipped.videoSamples,
|
|
361
|
+
audioSamples: lastClipped.audioSamples,
|
|
362
|
+
sequenceNumber: overlapping.length,
|
|
363
|
+
videoTimescale: PTS_PER_SECOND, audioTimescale,
|
|
364
|
+
videoBaseTime: Math.round(timelineOffset * PTS_PER_SECOND),
|
|
365
|
+
audioBaseTime: Math.round(timelineOffset * audioTimescale),
|
|
366
|
+
audioSampleDuration: 1024,
|
|
367
|
+
});
|
|
368
|
+
clipSegments.push({
|
|
369
|
+
duration: lastClipped.duration, data: lastFragment,
|
|
370
|
+
originalUrl: null, timelineOffset, isBoundary: true,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return { initSegment, clipSegments, audioTimescale };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── fMP4 variant processing ───────────────────────────────
|
|
379
|
+
|
|
380
|
+
function _processFmp4Variant({ firstSegData, lastSegData, fmp4Init, overlapping, isSingleSegment, startTime, endTime, firstSeg, lastSeg, log }) {
|
|
381
|
+
// For fMP4 sources: the init segment already has the moov with codec info.
|
|
382
|
+
// We pass it through as-is. Boundary segments are clipped using the fMP4
|
|
383
|
+
// converter. Middle segments pass through unchanged.
|
|
384
|
+
|
|
385
|
+
if (!fmp4Init) throw new Error('fMP4 source requires an init segment (#EXT-X-MAP)');
|
|
386
|
+
|
|
387
|
+
// Use the source init segment directly (it has the correct moov)
|
|
388
|
+
const initSegment = fmp4Init;
|
|
389
|
+
|
|
390
|
+
// Detect audio timescale from the init segment's moov
|
|
391
|
+
let audioTimescale = 48000;
|
|
392
|
+
try {
|
|
393
|
+
const boxes = parseBoxes(fmp4Init);
|
|
394
|
+
const moov = findBox(boxes, 'moov');
|
|
395
|
+
if (moov) {
|
|
396
|
+
const moovChildren = parseChildBoxes(moov);
|
|
397
|
+
for (const child of moovChildren) {
|
|
398
|
+
if (child.type === 'trak') {
|
|
399
|
+
const trakChildren = parseChildBoxes(child);
|
|
400
|
+
for (const tc of trakChildren) {
|
|
401
|
+
if (tc.type === 'mdia') {
|
|
402
|
+
const mdiaChildren = parseChildBoxes(tc);
|
|
403
|
+
let isSoun = false;
|
|
404
|
+
for (const mc of mdiaChildren) {
|
|
405
|
+
if (mc.type === 'hdlr' && mc.data.byteLength >= 20) {
|
|
406
|
+
const handler = String.fromCharCode(mc.data[16], mc.data[17], mc.data[18], mc.data[19]);
|
|
407
|
+
if (handler === 'soun') isSoun = true;
|
|
408
|
+
}
|
|
409
|
+
if (mc.type === 'mdhd' && isSoun) {
|
|
410
|
+
const v = new DataView(mc.data.buffer, mc.data.byteOffset, mc.data.byteLength);
|
|
411
|
+
audioTimescale = mc.data[8] === 0 ? v.getUint32(20) : v.getUint32(28);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
} catch (e) { /* use default */ }
|
|
420
|
+
|
|
421
|
+
const clipSegments = [];
|
|
422
|
+
let timelineOffset = 0;
|
|
423
|
+
|
|
424
|
+
// First segment: clip using fMP4 converter
|
|
425
|
+
const firstRelStart = startTime - firstSeg.startTime;
|
|
426
|
+
const firstRelEnd = isSingleSegment ? endTime - firstSeg.startTime : undefined;
|
|
427
|
+
|
|
428
|
+
// Combine init + first segment for the converter
|
|
429
|
+
const firstCombined = new Uint8Array(fmp4Init.byteLength + firstSegData.byteLength);
|
|
430
|
+
firstCombined.set(fmp4Init, 0);
|
|
431
|
+
firstCombined.set(firstSegData, fmp4Init.byteLength);
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
// Use convertFmp4ToMp4 with clipping, then re-fragment
|
|
435
|
+
// Actually, we can just pass the raw fMP4 segment through — for boundary
|
|
436
|
+
// segments, we trim at the keyframe level (no smart rendering for fMP4 yet).
|
|
437
|
+
// The segment already starts at a keyframe (HLS requirement).
|
|
438
|
+
|
|
439
|
+
// For the first segment, just pass through — the startTime cut is at keyframe
|
|
440
|
+
// For frame accuracy with fMP4, we'd need to add edit lists to the init segment
|
|
441
|
+
// or do smart rendering. For now, keyframe-accurate is the fMP4 path.
|
|
442
|
+
clipSegments.push({
|
|
443
|
+
duration: (firstRelEnd || firstSeg.duration) - firstRelStart,
|
|
444
|
+
data: firstSegData, // pass through the fMP4 segment
|
|
445
|
+
originalUrl: null, timelineOffset: 0, isBoundary: true,
|
|
446
|
+
_sourceFormat: 'fmp4',
|
|
447
|
+
});
|
|
448
|
+
timelineOffset += clipSegments[0].duration;
|
|
449
|
+
} catch (e) {
|
|
450
|
+
log('fMP4 first segment processing error: ' + e.message);
|
|
451
|
+
// Fallback: pass through as-is
|
|
452
|
+
clipSegments.push({
|
|
453
|
+
duration: firstSeg.duration, data: firstSegData,
|
|
454
|
+
originalUrl: null, timelineOffset: 0, isBoundary: true,
|
|
455
|
+
_sourceFormat: 'fmp4',
|
|
456
|
+
});
|
|
457
|
+
timelineOffset += firstSeg.duration;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Middle segments: pass through unchanged (already fMP4!)
|
|
461
|
+
for (let i = 1; i < overlapping.length - 1; i++) {
|
|
462
|
+
clipSegments.push({
|
|
463
|
+
duration: overlapping[i].duration, data: null,
|
|
464
|
+
originalUrl: overlapping[i].url, timelineOffset, isBoundary: false,
|
|
465
|
+
_sourceFormat: 'fmp4',
|
|
466
|
+
});
|
|
467
|
+
timelineOffset += overlapping[i].duration;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Last segment: pass through (truncation at end is handled by player)
|
|
471
|
+
if (!isSingleSegment && lastSegData) {
|
|
472
|
+
const lastRelEnd = endTime - lastSeg.startTime;
|
|
473
|
+
clipSegments.push({
|
|
474
|
+
duration: Math.min(lastRelEnd, lastSeg.duration),
|
|
475
|
+
data: lastSegData,
|
|
476
|
+
originalUrl: null, timelineOffset, isBoundary: true,
|
|
477
|
+
_sourceFormat: 'fmp4',
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return { initSegment, clipSegments, audioTimescale };
|
|
482
|
+
}
|
|
483
|
+
|
|
289
484
|
// ── main function ─────────────────────────────────────────
|
|
290
485
|
|
|
291
486
|
/**
|
|
@@ -362,107 +557,52 @@ export async function clipHls(source, options = {}) {
|
|
|
362
557
|
|
|
363
558
|
log(`Segments: ${overlapping.length} (${firstSeg.startTime.toFixed(1)}s – ${lastSeg.endTime.toFixed(1)}s)`);
|
|
364
559
|
|
|
365
|
-
// Download
|
|
560
|
+
// Download first boundary segment to detect format
|
|
366
561
|
log('Downloading boundary segments...');
|
|
367
|
-
const
|
|
368
|
-
const firstParser = parseTs(firstTsData);
|
|
562
|
+
const firstSegData = new Uint8Array(await (await fetch(firstSeg.url)).arrayBuffer());
|
|
369
563
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
lastTsData = new Uint8Array(await (await fetch(lastSeg.url)).arrayBuffer());
|
|
374
|
-
lastParser = parseTs(lastTsData);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Extract codec info from first segment
|
|
378
|
-
const { sps, pps } = extractCodecInfo(firstParser);
|
|
379
|
-
if (!sps || !pps) throw new Error('Could not extract SPS/PPS from video');
|
|
380
|
-
const audioSampleRate = firstParser.audioSampleRate || 48000;
|
|
381
|
-
const audioChannels = firstParser.audioChannels || 2;
|
|
382
|
-
const hasAudio = firstParser.audioAccessUnits.length > 0;
|
|
383
|
-
const audioTimescale = audioSampleRate;
|
|
384
|
-
|
|
385
|
-
// Create CMAF init segment
|
|
386
|
-
const initSegment = createInitSegment({
|
|
387
|
-
sps, pps, audioSampleRate, audioChannels, hasAudio,
|
|
388
|
-
videoTimescale: PTS_PER_SECOND,
|
|
389
|
-
audioTimescale,
|
|
390
|
-
});
|
|
564
|
+
// Detect source format: TS or fMP4
|
|
565
|
+
const sourceFormat = _detectSegmentFormat(firstSegData);
|
|
566
|
+
const isFmp4Source = sourceFormat === 'fmp4';
|
|
391
567
|
|
|
392
|
-
|
|
393
|
-
const clipSegments = [];
|
|
394
|
-
let timelineOffset = 0;
|
|
395
|
-
|
|
396
|
-
// ── First segment (clipped at start, possibly also at end) ──
|
|
397
|
-
// Convert absolute times to segment-relative times (TS PTS starts at ~0 per segment)
|
|
398
|
-
const firstRelStart = startTime - firstSeg.startTime;
|
|
399
|
-
const firstRelEnd = isSingleSegment ? endTime - firstSeg.startTime : undefined;
|
|
400
|
-
const firstClipped = clipSegment(firstParser, firstRelStart, firstRelEnd);
|
|
401
|
-
if (!firstClipped) throw new Error('First segment clip produced no samples');
|
|
402
|
-
|
|
403
|
-
const firstFragment = createFragment({
|
|
404
|
-
videoSamples: firstClipped.videoSamples,
|
|
405
|
-
audioSamples: firstClipped.audioSamples,
|
|
406
|
-
sequenceNumber: 1,
|
|
407
|
-
videoTimescale: PTS_PER_SECOND,
|
|
408
|
-
audioTimescale,
|
|
409
|
-
videoBaseTime: 0,
|
|
410
|
-
audioBaseTime: 0,
|
|
411
|
-
audioSampleDuration: 1024,
|
|
412
|
-
});
|
|
568
|
+
log(`Source format: ${isFmp4Source ? 'fMP4 (CMAF)' : 'MPEG-TS'}`);
|
|
413
569
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
570
|
+
// Download fMP4 init segment if needed
|
|
571
|
+
let fmp4Init = null;
|
|
572
|
+
if (isFmp4Source && initSegmentUrl) {
|
|
573
|
+
const initResp = await fetch(initSegmentUrl);
|
|
574
|
+
if (initResp.ok) {
|
|
575
|
+
fmp4Init = new Uint8Array(await initResp.arrayBuffer());
|
|
576
|
+
}
|
|
577
|
+
}
|
|
422
578
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const segDuration = seg.duration;
|
|
427
|
-
clipSegments.push({
|
|
428
|
-
duration: segDuration,
|
|
429
|
-
data: null, // fetched on demand
|
|
430
|
-
originalUrl: seg.url,
|
|
431
|
-
timelineOffset,
|
|
432
|
-
isBoundary: false,
|
|
433
|
-
});
|
|
434
|
-
timelineOffset += segDuration;
|
|
579
|
+
let lastSegData = null;
|
|
580
|
+
if (!isSingleSegment) {
|
|
581
|
+
lastSegData = new Uint8Array(await (await fetch(lastSeg.url)).arrayBuffer());
|
|
435
582
|
}
|
|
436
583
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
duration: lastClipped.duration,
|
|
460
|
-
data: lastFragment,
|
|
461
|
-
originalUrl: null,
|
|
462
|
-
timelineOffset,
|
|
463
|
-
isBoundary: true,
|
|
464
|
-
});
|
|
465
|
-
}
|
|
584
|
+
let initSegment, clipSegments, audioTimescale;
|
|
585
|
+
|
|
586
|
+
if (isFmp4Source) {
|
|
587
|
+
// ── fMP4 source path ────────────────────────────────
|
|
588
|
+
const result = _processFmp4Variant({
|
|
589
|
+
firstSegData, lastSegData, fmp4Init,
|
|
590
|
+
overlapping, isSingleSegment,
|
|
591
|
+
startTime, endTime, firstSeg, lastSeg, log,
|
|
592
|
+
});
|
|
593
|
+
initSegment = result.initSegment;
|
|
594
|
+
clipSegments = result.clipSegments;
|
|
595
|
+
audioTimescale = result.audioTimescale;
|
|
596
|
+
} else {
|
|
597
|
+
// ── TS source path (existing smart-render pipeline) ──
|
|
598
|
+
const result = _processTsVariant({
|
|
599
|
+
firstSegData, lastSegData,
|
|
600
|
+
overlapping, isSingleSegment,
|
|
601
|
+
startTime, endTime, firstSeg, lastSeg, log,
|
|
602
|
+
});
|
|
603
|
+
initSegment = result.initSegment;
|
|
604
|
+
clipSegments = result.clipSegments;
|
|
605
|
+
audioTimescale = result.audioTimescale;
|
|
466
606
|
}
|
|
467
607
|
|
|
468
608
|
const totalDuration = clipSegments.reduce((sum, s) => sum + s.duration, 0);
|
package/src/index.js
CHANGED