@flow-player/software-decoder-ffmpeg 0.14.2 → 0.15.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.
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: (_nativeTimestamp, yPointer, uPointer, vPointer, yStride, uStride, vStride) => {
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 result = decoder.setCodec("hvc", description);
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
- * 输出帧时间戳取毫秒,回退链为 `timestampMs ?? ptsMs ?? timestamp / 1000`:
98
- * 优先用包携带的 timestampMs;缺失时回退 ptsMs(同为毫秒);再缺失则以
99
- * timestamp / 1000 兜底(timestamp 为微秒,与 WebCodecs EncodedVideoChunk
100
- * 的 timestamp 同单位)。回退结果与 timestamp 原值一起进入 pending 队列,
101
- * 输出帧按"最早 pending"取出,保证 B 帧重排下时间戳与画面对齐。
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(decoder, packet.payload, packet.timestamp >>> 0);
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.#removePendingTimestamp(pending);
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(payload.byteLength);
128
- if (payload.byteLength > 0 && (!Number.isInteger(pointer) || pointer <= 0)) {
129
- throw new Error(`FFmpeg input allocation failed: ${payload.byteLength} bytes`);
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(payload, pointer);
132
- return decoder.decodeInput(payload.byteLength, timestamp);
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(payload, timestamp);
172
+ return decoder.decode(fed, timestamp);
138
173
  }
139
174
  /**
140
- * 有意的 no-op:协议里的 flush 面向"内部有异步输出队列"的解码器,而本 adapter
141
- * 的帧在 decode() 调用内同步经 draw 回调吐出,flush 时没有可排空的在途输出;
142
- * ffdecoder embind 句柄也没有 EOF/drain 原语(仅 setCodec/seek/clear/delete),
143
- * 无法把 FFmpeg 内部帧缓冲显式排空。runtime 侧真正的解码器清理由 configure
144
- * 全新重建句柄完成(seek 路径刻意不走 reset/flush,见 browser-runtime
145
- * software-decode-controller 的 avcodec_flush_buffers 约束),故这里保持空实现。
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: outputTimestamp.timestamp,
216
- timestampMs: outputTimestamp.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
- #takeOutputTimestamp() {
256
- if (this.#pendingTimestamps.length === 0) return { timestamp: 0, timestampMs: 0 };
257
- let earliestIndex = 0;
258
- for (let i = 1; i < this.#pendingTimestamps.length; i++) {
259
- if (this.#pendingTimestamps[i].timestamp < this.#pendingTimestamps[earliestIndex].timestamp) {
260
- earliestIndex = i;
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.14.2",
3
+ "version": "0.15.0",
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.14.2"
37
+ "@flow-player/browser-runtime": "^0.15.0"
38
38
  }
39
39
  }