@flow-player/software-decoder-ffmpeg 0.14.2 → 0.15.1
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/ffdecoder-simd.wasm
CHANGED
|
Binary file
|
|
@@ -40,6 +40,13 @@ var FfmpegSimdSoftwareDecoder = class {
|
|
|
40
40
|
#acquireFrameBuffers;
|
|
41
41
|
#viewFrames;
|
|
42
42
|
#pendingTimestamps = [];
|
|
43
|
+
/** hvcC 的 NALU 长度前缀字节数(lengthSizeMinusOne + 1);0 表示非 hvcC 契约
|
|
44
|
+
* (描述为 Annex B 或空),decode 不做包格式改写。 */
|
|
45
|
+
#mp4NalLengthSize = 0;
|
|
46
|
+
/** hvcC 转出的 Annex B 参数集(VPS/SPS/PPS 起始码拼接)。FFmpeg 的 Annex B
|
|
47
|
+
* 契约下参数集必须物理存在于流中(extradata 预载不可靠——hvcC 转出的文件
|
|
48
|
+
* GOP 关键帧常不带内联参数集,出首帧后即停摆),按关键帧前置注入。 */
|
|
49
|
+
#annexbParams = null;
|
|
43
50
|
/** 创建实例并立即构建原生解码句柄(尚未配置 codec,configure 前不可 decode)。 */
|
|
44
51
|
constructor(module, options) {
|
|
45
52
|
this.#module = module;
|
|
@@ -55,8 +62,8 @@ var FfmpegSimdSoftwareDecoder = class {
|
|
|
55
62
|
this.#width = width;
|
|
56
63
|
this.#height = height;
|
|
57
64
|
},
|
|
58
|
-
draw: (
|
|
59
|
-
this.#emitFrame(yPointer, uPointer, vPointer, yStride, uStride, vStride);
|
|
65
|
+
draw: (ptsUs, yPointer, uPointer, vPointer, yStride, uStride, vStride) => {
|
|
66
|
+
this.#emitFrame(ptsUs, yPointer, uPointer, vPointer, yStride, uStride, vStride);
|
|
60
67
|
}
|
|
61
68
|
});
|
|
62
69
|
}
|
|
@@ -86,7 +93,10 @@ var FfmpegSimdSoftwareDecoder = class {
|
|
|
86
93
|
this.#configured = false;
|
|
87
94
|
}
|
|
88
95
|
const decoder = this.#decoder ??= this.#createNativeDecoder();
|
|
89
|
-
const
|
|
96
|
+
const isHvcC = description.length >= 23 && description[0] === 1;
|
|
97
|
+
this.#mp4NalLengthSize = isHvcC ? (description[21] & 3) + 1 : 0;
|
|
98
|
+
this.#annexbParams = isHvcC ? hevcHvcCToAnnexB(description) : null;
|
|
99
|
+
const result = decoder.setCodec("hvc", isHvcC ? hevcHvcCToAnnexB(description) : description);
|
|
90
100
|
if (result !== 0) throw new Error(`FFmpeg setCodec failed: ${result}`);
|
|
91
101
|
this.#configured = true;
|
|
92
102
|
}
|
|
@@ -94,11 +104,11 @@ var FfmpegSimdSoftwareDecoder = class {
|
|
|
94
104
|
* 解码一个视频包;FFmpeg 解出帧时在本次调用内同步触发 onFrame(可能一次
|
|
95
105
|
* 吐出积压的有序帧,也可能本次无输出)。
|
|
96
106
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
107
|
+
* 时间戳直通:包的 `timestamp`(微秒,不截断)经 `submitPacket` 以微秒 timebase
|
|
108
|
+
* 送入 FFmpeg,输出帧 pts 由 C++ 侧换算为微秒后经 draw 首参回传,adapter 直接
|
|
109
|
+
* 采用为 onFrame 的 timestamp(timestampMs = ptsUs / 1000)。pending 账本只做
|
|
110
|
+
* 提交记账(pendingCount 可观测),不再参与画面时间配对。句柄无 submitPacket
|
|
111
|
+
* 的旧构建回退 legacy 路径并打 console.warn。
|
|
102
112
|
*
|
|
103
113
|
* @throws 已 close("decoder closed")或尚未 configure("decoder is not configured")
|
|
104
114
|
* @throws ffdecoder 输入暂存分配失败 / 解码失败(非零返回码);错误先经
|
|
@@ -108,43 +118,78 @@ var FfmpegSimdSoftwareDecoder = class {
|
|
|
108
118
|
if (this.#closed) throw new Error("decoder closed");
|
|
109
119
|
const decoder = this.#decoder;
|
|
110
120
|
if (!this.#configured || !decoder) throw new Error("decoder is not configured");
|
|
121
|
+
if (packet.payload.byteLength === 0) return;
|
|
111
122
|
const pending = {
|
|
112
123
|
timestamp: packet.timestamp,
|
|
113
124
|
timestampMs: packet.timestampMs ?? packet.ptsMs ?? packet.timestamp / 1e3
|
|
114
125
|
};
|
|
115
126
|
this.#pendingTimestamps.push(pending);
|
|
116
127
|
try {
|
|
117
|
-
const result = this.#decodePacket(
|
|
128
|
+
const result = this.#decodePacket(
|
|
129
|
+
decoder,
|
|
130
|
+
packet.payload,
|
|
131
|
+
packet.timestamp,
|
|
132
|
+
packet.isKeyframe === true ? this.#annexbParams : null
|
|
133
|
+
);
|
|
118
134
|
if (result !== 0) throw new Error(`FFmpeg decode failed: ${result}`);
|
|
119
135
|
} catch (error) {
|
|
120
|
-
this.#
|
|
136
|
+
const index = this.#pendingTimestamps.indexOf(pending);
|
|
137
|
+
if (index >= 0) {
|
|
138
|
+
const lastIndex = this.#pendingTimestamps.length - 1;
|
|
139
|
+
this.#pendingTimestamps[index] = this.#pendingTimestamps[lastIndex];
|
|
140
|
+
this.#pendingTimestamps.length = lastIndex;
|
|
141
|
+
}
|
|
121
142
|
this.#onError(error);
|
|
122
143
|
throw error;
|
|
123
144
|
}
|
|
124
145
|
}
|
|
125
|
-
#decodePacket(decoder, payload, timestamp) {
|
|
146
|
+
#decodePacket(decoder, payload, timestamp, prependParams) {
|
|
147
|
+
const annexb = this.#mp4NalLengthSize > 0 ? hevcLengthPrefixedToAnnexB(payload, this.#mp4NalLengthSize) : payload;
|
|
148
|
+
const fed = prependParams && prependParams.length > 0 ? concatUint8Arrays(prependParams, annexb) : annexb;
|
|
149
|
+
if (decoder.submitPacket) {
|
|
150
|
+
if (!decoder.reserveInput) {
|
|
151
|
+
throw new Error("FFmpeg decoder does not expose reserveInput()");
|
|
152
|
+
}
|
|
153
|
+
const pointer = decoder.reserveInput(fed.byteLength);
|
|
154
|
+
if (fed.byteLength > 0 && (!Number.isInteger(pointer) || pointer <= 0)) {
|
|
155
|
+
throw new Error(`FFmpeg input allocation failed: ${fed.byteLength} bytes`);
|
|
156
|
+
}
|
|
157
|
+
this.#module.HEAPU8.set(fed, pointer);
|
|
158
|
+
return decoder.submitPacket(fed.byteLength, timestamp, -1, 1, 1e6);
|
|
159
|
+
}
|
|
160
|
+
console.warn("[ffmpeg-simd] submitPacket unavailable, falling back to legacy decodeInput");
|
|
126
161
|
if (decoder.reserveInput && decoder.decodeInput) {
|
|
127
|
-
const pointer = decoder.reserveInput(
|
|
128
|
-
if (
|
|
129
|
-
throw new Error(`FFmpeg input allocation failed: ${
|
|
162
|
+
const pointer = decoder.reserveInput(fed.byteLength);
|
|
163
|
+
if (fed.byteLength > 0 && (!Number.isInteger(pointer) || pointer <= 0)) {
|
|
164
|
+
throw new Error(`FFmpeg input allocation failed: ${fed.byteLength} bytes`);
|
|
130
165
|
}
|
|
131
|
-
this.#module.HEAPU8.set(
|
|
132
|
-
return decoder.decodeInput(
|
|
166
|
+
this.#module.HEAPU8.set(fed, pointer);
|
|
167
|
+
return decoder.decodeInput(fed.byteLength, timestamp);
|
|
133
168
|
}
|
|
134
169
|
if (!decoder.decode) {
|
|
135
170
|
throw new Error("FFmpeg decoder does not expose decodeInput() or decode()");
|
|
136
171
|
}
|
|
137
|
-
return decoder.decode(
|
|
172
|
+
return decoder.decode(fed, timestamp);
|
|
138
173
|
}
|
|
139
174
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
175
|
+
* 排空解码器内延迟帧:调 ffdecoder 句柄 `drain()`(发空包刷出),同步回调内
|
|
176
|
+
* 吐出的帧照常走 onFrame;返回本次刷出的帧数。
|
|
177
|
+
*
|
|
178
|
+
* @returns drain 刷出的帧数(0 表示无延迟帧)
|
|
179
|
+
* @throws 已 close("decoder closed")或尚未 configure("decoder is not configured")
|
|
180
|
+
* @throws 句柄无 drain 原语(旧构建缺 P2 胶水,要求重建 wasm 产物)
|
|
181
|
+
* @throws drain 返回负值(FFmpeg 错误码)时抛 `FFmpeg drain failed: <码>`
|
|
146
182
|
*/
|
|
147
183
|
flush() {
|
|
184
|
+
if (this.#closed) throw new Error("decoder closed");
|
|
185
|
+
const decoder = this.#decoder;
|
|
186
|
+
if (!this.#configured || !decoder) throw new Error("decoder is not configured");
|
|
187
|
+
if (!decoder.drain) {
|
|
188
|
+
throw new Error("FFmpeg decoder does not expose drain() (requires P2 decoder build)");
|
|
189
|
+
}
|
|
190
|
+
const result = decoder.drain();
|
|
191
|
+
if (result < 0) throw new Error(`FFmpeg drain failed: ${result}`);
|
|
192
|
+
return result;
|
|
148
193
|
}
|
|
149
194
|
/**
|
|
150
195
|
* 软复位:丢弃 pending 时间戳并调用 ffdecoder seek() 清空解码器内部状态
|
|
@@ -189,7 +234,7 @@ var FfmpegSimdSoftwareDecoder = class {
|
|
|
189
234
|
console.warn("[ffmpeg-simd] decoder delete failed during release", error);
|
|
190
235
|
}
|
|
191
236
|
}
|
|
192
|
-
#emitFrame(yPointer, uPointer, vPointer, yStride = this.#width, uStride = Math.ceil(this.#width / 2), vStride = Math.ceil(this.#width / 2)) {
|
|
237
|
+
#emitFrame(ptsUs, yPointer, uPointer, vPointer, yStride = this.#width, uStride = Math.ceil(this.#width / 2), vStride = Math.ceil(this.#width / 2)) {
|
|
193
238
|
const width = this.#width;
|
|
194
239
|
const height = this.#height;
|
|
195
240
|
if (width <= 0 || height <= 0) {
|
|
@@ -208,12 +253,11 @@ var FfmpegSimdSoftwareDecoder = class {
|
|
|
208
253
|
const y = this.#planeData(yPointer, width, height, yStride, pooled?.[0]);
|
|
209
254
|
const u = this.#planeData(uPointer, chromaWidth, chromaHeight, uStride, pooled?.[1]);
|
|
210
255
|
const v = this.#planeData(vPointer, chromaWidth, chromaHeight, vStride, pooled?.[2]);
|
|
211
|
-
const outputTimestamp = this.#takeOutputTimestamp();
|
|
212
256
|
this.#onFrame({
|
|
213
257
|
width,
|
|
214
258
|
height,
|
|
215
|
-
timestamp:
|
|
216
|
-
timestampMs:
|
|
259
|
+
timestamp: ptsUs,
|
|
260
|
+
timestampMs: ptsUs / 1e3,
|
|
217
261
|
format: "I420",
|
|
218
262
|
planes: [
|
|
219
263
|
{ data: y, stride: this.#viewFrames ? yStride : width },
|
|
@@ -252,26 +296,12 @@ var FfmpegSimdSoftwareDecoder = class {
|
|
|
252
296
|
getWasmMemoryBytes() {
|
|
253
297
|
return this.#module.HEAPU8.buffer.byteLength;
|
|
254
298
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
const lastIndex = this.#pendingTimestamps.length - 1;
|
|
264
|
-
const timestamp = this.#pendingTimestamps[earliestIndex];
|
|
265
|
-
this.#pendingTimestamps[earliestIndex] = this.#pendingTimestamps[lastIndex];
|
|
266
|
-
this.#pendingTimestamps.length = lastIndex;
|
|
267
|
-
return timestamp;
|
|
268
|
-
}
|
|
269
|
-
#removePendingTimestamp(timestamp) {
|
|
270
|
-
const index = this.#pendingTimestamps.indexOf(timestamp);
|
|
271
|
-
if (index < 0) return;
|
|
272
|
-
const lastIndex = this.#pendingTimestamps.length - 1;
|
|
273
|
-
this.#pendingTimestamps[index] = this.#pendingTimestamps[lastIndex];
|
|
274
|
-
this.#pendingTimestamps.length = lastIndex;
|
|
299
|
+
/**
|
|
300
|
+
* 提交记账数(诊断用):自上次 configure/reset/close 以来 decode 提交的包数
|
|
301
|
+
* (失败提交已剔除)。只做任务/资源记账,不参与画面时间戳配对。
|
|
302
|
+
*/
|
|
303
|
+
get pendingCount() {
|
|
304
|
+
return this.#pendingTimestamps.length;
|
|
275
305
|
}
|
|
276
306
|
};
|
|
277
307
|
async function loadFfmpegAssets() {
|
|
@@ -365,6 +395,86 @@ return module.exports;`
|
|
|
365
395
|
}
|
|
366
396
|
return module;
|
|
367
397
|
}
|
|
398
|
+
function hevcLengthPrefixedToAnnexB(payload, lengthSize) {
|
|
399
|
+
if (lengthSize < 1 || lengthSize > 4) {
|
|
400
|
+
throw new Error(`invalid HEVC NALU length size: ${lengthSize}`);
|
|
401
|
+
}
|
|
402
|
+
const nalus = [];
|
|
403
|
+
let pos = 0;
|
|
404
|
+
while (pos + lengthSize <= payload.length) {
|
|
405
|
+
let length = 0;
|
|
406
|
+
for (let k = 0; k < lengthSize; k += 1) {
|
|
407
|
+
length = length << 8 | payload[pos + k];
|
|
408
|
+
}
|
|
409
|
+
pos += lengthSize;
|
|
410
|
+
if (length <= 0 || pos + length > payload.length) {
|
|
411
|
+
throw new Error(`HEVC payload NALU length prefix out of bounds at ${pos - lengthSize}`);
|
|
412
|
+
}
|
|
413
|
+
nalus.push([pos, length]);
|
|
414
|
+
pos += length;
|
|
415
|
+
}
|
|
416
|
+
if (pos !== payload.length || nalus.length === 0) {
|
|
417
|
+
throw new Error(`HEVC payload trailing bytes after last NALU at ${pos}`);
|
|
418
|
+
}
|
|
419
|
+
const output = new Uint8Array(nalus.reduce((sum, [, length]) => sum + 4 + length, 0));
|
|
420
|
+
let offset = 0;
|
|
421
|
+
for (const [start, length] of nalus) {
|
|
422
|
+
output[offset] = 0;
|
|
423
|
+
output[offset + 1] = 0;
|
|
424
|
+
output[offset + 2] = 0;
|
|
425
|
+
output[offset + 3] = 1;
|
|
426
|
+
output.set(payload.subarray(start, start + length), offset + 4);
|
|
427
|
+
offset += 4 + length;
|
|
428
|
+
}
|
|
429
|
+
return output;
|
|
430
|
+
}
|
|
431
|
+
function concatUint8Arrays(a, b) {
|
|
432
|
+
const out = new Uint8Array(a.length + b.length);
|
|
433
|
+
out.set(a, 0);
|
|
434
|
+
out.set(b, a.length);
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
437
|
+
function hevcHvcCToAnnexB(description) {
|
|
438
|
+
if (description.length < 23 || description[0] !== 1) return description;
|
|
439
|
+
const numOfArrays = description[22];
|
|
440
|
+
const nalus = [];
|
|
441
|
+
let pos = 23;
|
|
442
|
+
let valid = true;
|
|
443
|
+
scan: for (let i = 0; i < numOfArrays; i += 1) {
|
|
444
|
+
if (pos + 3 > description.length) {
|
|
445
|
+
valid = false;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
const numNalus = description[pos + 1] << 8 | description[pos + 2];
|
|
449
|
+
pos += 3;
|
|
450
|
+
for (let j = 0; j < numNalus; j += 1) {
|
|
451
|
+
if (pos + 2 > description.length) {
|
|
452
|
+
valid = false;
|
|
453
|
+
break scan;
|
|
454
|
+
}
|
|
455
|
+
const nalLength = description[pos] << 8 | description[pos + 1];
|
|
456
|
+
pos += 2;
|
|
457
|
+
if (pos + nalLength > description.length) {
|
|
458
|
+
valid = false;
|
|
459
|
+
break scan;
|
|
460
|
+
}
|
|
461
|
+
nalus.push([pos, nalLength]);
|
|
462
|
+
pos += nalLength;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (!valid || nalus.length === 0) return description;
|
|
466
|
+
const output = new Uint8Array(nalus.reduce((sum, [, length]) => sum + 4 + length, 0));
|
|
467
|
+
let offset = 0;
|
|
468
|
+
for (const [start, length] of nalus) {
|
|
469
|
+
output[offset] = 0;
|
|
470
|
+
output[offset + 1] = 0;
|
|
471
|
+
output[offset + 2] = 0;
|
|
472
|
+
output[offset + 3] = 1;
|
|
473
|
+
output.set(description.subarray(start, start + length), offset + 4);
|
|
474
|
+
offset += 4 + length;
|
|
475
|
+
}
|
|
476
|
+
return output;
|
|
477
|
+
}
|
|
368
478
|
async function createSoftwareVideoDecoder(options) {
|
|
369
479
|
const module = await loadFfmpegSimdModule(options);
|
|
370
480
|
return new FfmpegSimdSoftwareDecoder(module, options);
|
|
@@ -374,5 +484,7 @@ export {
|
|
|
374
484
|
FfmpegSimdSoftwareDecoder,
|
|
375
485
|
createSoftwareVideoDecoder,
|
|
376
486
|
ffmpeg_software_decoder_adapter_default as default,
|
|
487
|
+
hevcHvcCToAnnexB,
|
|
488
|
+
hevcLengthPrefixedToAnnexB,
|
|
377
489
|
resolveSoftwareDecoderWasmUrl
|
|
378
490
|
};
|
|
@@ -54,6 +54,8 @@ var SOFTWARE_DECODE_WORKER_MESSAGE = Object.freeze({
|
|
|
54
54
|
Packet: "packet",
|
|
55
55
|
/** in: adapter.reset(ack: ResetDone)。 */
|
|
56
56
|
Reset: "reset",
|
|
57
|
+
/** in: adapter.drain 刷滞留延迟帧(ack: DrainDone,载荷 frames 为刷出帧数)。 */
|
|
58
|
+
Drain: "drain",
|
|
57
59
|
/** in: adapter.close。 */
|
|
58
60
|
Close: "close",
|
|
59
61
|
/** in: 归还已消费 I420 帧的 y/u/v ArrayBuffer(transfer),供帧缓冲池复用。 */
|
|
@@ -65,6 +67,8 @@ var SOFTWARE_DECODE_WORKER_MESSAGE = Object.freeze({
|
|
|
65
67
|
DecodeDone: "decode-done",
|
|
66
68
|
ConfigureDone: "configure-done",
|
|
67
69
|
ResetDone: "reset-done",
|
|
70
|
+
/** out: Drain 的响应(payload: frames 刷出帧数)。 */
|
|
71
|
+
DrainDone: "drain-done",
|
|
68
72
|
Error: "error",
|
|
69
73
|
/** out: 定期上报解码器 wasm heap 字节数(payload: bytes)。 */
|
|
70
74
|
WasmMemory: "wasm-memory",
|
|
@@ -202,6 +206,16 @@ self.onmessage = (event) => {
|
|
|
202
206
|
post({ type: SOFTWARE_DECODE_WORKER_MESSAGE.ResetDone, id: message.id });
|
|
203
207
|
break;
|
|
204
208
|
}
|
|
209
|
+
case SOFTWARE_DECODE_WORKER_MESSAGE.Drain: {
|
|
210
|
+
if (!decoder) throw new Error("decoder not ready");
|
|
211
|
+
const drain = decoder.drain;
|
|
212
|
+
if (typeof drain !== "function") {
|
|
213
|
+
throw new Error("software decoder does not implement drain()");
|
|
214
|
+
}
|
|
215
|
+
const frames = await drain.call(decoder);
|
|
216
|
+
post({ type: SOFTWARE_DECODE_WORKER_MESSAGE.DrainDone, id: message.id, frames });
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
205
219
|
case SOFTWARE_DECODE_WORKER_MESSAGE.Close: {
|
|
206
220
|
stopWasmMemoryReporting();
|
|
207
221
|
await decoder?.close?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flow-player/software-decoder-ffmpeg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "FFmpeg (ffdecoder) WASM software decode backend for @flow-player/browser-runtime: prebuilt adapter + decode worker, drop-in HEVC software decoding.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esm",
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"tag": "latest"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@flow-player/browser-runtime": "^0.
|
|
37
|
+
"@flow-player/browser-runtime": "^0.15.1"
|
|
38
38
|
}
|
|
39
39
|
}
|