@camstack/addon-pipeline 1.1.32 → 1.1.33

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.
Files changed (69) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/audio-codec-ffmpeg/index.js +118 -3
  4. package/dist/audio-codec-ffmpeg/index.mjs +118 -3
  5. package/dist/decoder-ffmpeg/index.js +71 -6
  6. package/dist/decoder-ffmpeg/index.mjs +71 -6
  7. package/dist/decoder-nodeav/index.js +209 -9
  8. package/dist/decoder-nodeav/index.mjs +209 -9
  9. package/dist/detection-pipeline/index.js +1 -1
  10. package/dist/detection-pipeline/index.mjs +1 -1
  11. package/dist/{dist-Cwc0TUQr.js → dist-BrIE85BG.js} +38 -1
  12. package/dist/{dist-DjuGmyG9.mjs → dist-CfSORkv2.mjs} +38 -1
  13. package/dist/{ffmpeg-args-C5GPp8Cw.mjs → ffmpeg-args-CpeRWwL4.mjs} +7 -4
  14. package/dist/{ffmpeg-args-D6h1edXK.js → ffmpeg-args-Dl9mLcOC.js} +7 -4
  15. package/dist/{frame-handle-plane-Bkxz-TTD.mjs → frame-handle-plane-C05rq9Zs.mjs} +108 -17
  16. package/dist/{frame-handle-plane-DQNCTrpC.js → frame-handle-plane-zMju9vmk.js} +108 -17
  17. package/dist/{frame-ring-sink-DvdhCFr3.js → frame-ring-sink-B-DxHelU.js} +74 -25
  18. package/dist/{frame-ring-sink-sY-8XUsH.mjs → frame-ring-sink-B_WjDX0x.mjs} +69 -26
  19. package/dist/motion-wasm/index.js +1 -1
  20. package/dist/motion-wasm/index.mjs +1 -1
  21. package/dist/pipeline-runner/index.js +86 -16
  22. package/dist/pipeline-runner/index.mjs +85 -17
  23. package/dist/recorder/index.js +1 -1
  24. package/dist/recorder/index.mjs +1 -1
  25. package/dist/stream-broker/_stub.js +1 -1
  26. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DPFAbcUo.mjs} +3 -3
  27. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DtoYGpTp.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BsrrdOBU.mjs} +1 -1
  28. package/dist/stream-broker/{hostInit-B_b3PIZB.mjs → hostInit-C8slrxw1.mjs} +3 -3
  29. package/dist/stream-broker/index.js +165 -27
  30. package/dist/stream-broker/index.mjs +165 -27
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js → MaskShapeCanvas-DI4BY7W2-WUVfFzT-.js} +1 -1
  33. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DTeuDGCU.js → MotionZonesSettings-NcxxQN8r-CF7FXXLs.js} +1 -1
  34. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CirtxO3e.js → PrivacyMaskSettings-APgPLF7p-qQCpVob3.js} +1 -1
  35. package/embed-dist/assets/{index-CpUy8OIE.js → index-BNhisVUZ.js} +4 -4
  36. package/embed-dist/index.html +1 -1
  37. package/package.json +1 -1
  38. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  39. package/python/__pycache__/inference_pool.cpython-314.pyc +0 -0
  40. package/python/__pycache__/test_inference_pool_backpressure.cpython-314.pyc +0 -0
  41. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-314.pyc +0 -0
  42. package/python/__pycache__/test_inference_pool_device_selection.cpython-314.pyc +0 -0
  43. package/python/__pycache__/yamnet_audio.cpython-314.pyc +0 -0
  44. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  45. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  46. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  47. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  48. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  49. package/python/postprocessors/__pycache__/clip.cpython-313.pyc +0 -0
  50. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  51. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  52. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  53. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  54. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  55. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  56. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  57. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  58. package/python/postprocessors/__pycache__/test_clip.cpython-313-pytest-9.1.1.pyc +0 -0
  59. package/python/postprocessors/__pycache__/test_saliency.cpython-313-pytest-9.1.1.pyc +0 -0
  60. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  61. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  62. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  63. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  64. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  65. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
  66. package/dist/constants-CAqYVigN.js +0 -6615
  67. package/dist/constants-oYgW36Ty.mjs +0 -3587
  68. package/dist/dist-D3Ytt-_Y.js +0 -41111
  69. package/dist/dist-DluJuxV5.mjs +0 -37976
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
6
- const require_dist = require("../dist-Cwc0TUQr.js");
6
+ const require_dist = require("../dist-BrIE85BG.js");
7
7
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
@@ -508,7 +508,7 @@ var AudioAnalyzerProvider = class {
508
508
  }
509
509
  this.classifyCallCount++;
510
510
  const meaningful = result.labels.filter((l) => l.score >= .15 && l.className.toLowerCase() !== "silence");
511
- if (meaningful.length > 0) this.log.info("audio classification", {
511
+ if (meaningful.length > 0) this.log.debug("audio classification", {
512
512
  tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
513
513
  meta: {
514
514
  top: meaningful.slice(0, 4).map((l) => `${l.className}(${(l.score * 100).toFixed(0)}%)`).join(", "),
@@ -1,5 +1,5 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { B as errMsg, D as mapAudioLabelToMacro, V as BaseAddon, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, q as hydrateSchema, u as HF_BASE_URL } from "../dist-DjuGmyG9.mjs";
2
+ import { B as errMsg, D as mapAudioLabelToMacro, V as BaseAddon, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, q as hydrateSchema, u as HF_BASE_URL } from "../dist-CfSORkv2.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
4
  import { r as downloadFile } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
5
5
  import * as fs from "node:fs";
@@ -503,7 +503,7 @@ var AudioAnalyzerProvider = class {
503
503
  }
504
504
  this.classifyCallCount++;
505
505
  const meaningful = result.labels.filter((l) => l.score >= .15 && l.className.toLowerCase() !== "silence");
506
- if (meaningful.length > 0) this.log.info("audio classification", {
506
+ if (meaningful.length > 0) this.log.debug("audio classification", {
507
507
  tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
508
508
  meta: {
509
509
  top: meaningful.slice(0, 4).map((l) => `${l.className}(${(l.score * 100).toFixed(0)}%)`).join(", "),
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-Cwc0TUQr.js");
5
+ const require_dist = require("../dist-BrIE85BG.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let node_child_process = require("node:child_process");
8
8
  //#region src/audio-codec-ffmpeg/ffmpeg-audio-process.ts
@@ -615,6 +615,82 @@ var OggOpusFramer = class {
615
615
  }
616
616
  };
617
617
  //#endregion
618
+ //#region src/audio-codec-ffmpeg/adts.ts
619
+ /**
620
+ * ADTS framing for raw AAC access units.
621
+ *
622
+ * The audio decode path runs `ffmpeg -f aac -i pipe:0` — the **ADTS** demuxer,
623
+ * which requires each frame to begin with a 7-byte ADTS header (0xFFF sync).
624
+ * But every AAC source that reaches the decoder delivers **raw** AAC access
625
+ * units with no ADTS header: push sources (Reolink Baichuan) emit one bare
626
+ * codec frame per packet, and rfc3640 (RTP AAC) depacketization yields bare
627
+ * AUs too. Piping those straight into `-f aac` fails with
628
+ * `Invalid data found when processing input` → no PCM → a silent WebRTC audio
629
+ * track. Wrapping each raw AU in an ADTS header (built from the source
630
+ * sample-rate / channel-count / AAC object type) makes the demuxer accept it.
631
+ *
632
+ * Opus is handled separately (Ogg encapsulation); this module is AAC-only.
633
+ */
634
+ /** MPEG-4 AAC sampling-frequency index table (ISO/IEC 14496-3). */
635
+ var AAC_SAMPLE_RATES = [
636
+ 96e3,
637
+ 88200,
638
+ 64e3,
639
+ 48e3,
640
+ 44100,
641
+ 32e3,
642
+ 24e3,
643
+ 22050,
644
+ 16e3,
645
+ 12e3,
646
+ 11025,
647
+ 8e3,
648
+ 7350
649
+ ];
650
+ /** AAC-LC is object type 2; the ADTS `profile` field is objectType - 1. */
651
+ var DEFAULT_AAC_OBJECT_TYPE = 2;
652
+ /** Resolve the ADTS sampling-frequency index for a sample rate (default 16 kHz → 8). */
653
+ function aacSampleRateIndex(sampleRate) {
654
+ const idx = AAC_SAMPLE_RATES.indexOf(sampleRate);
655
+ return idx >= 0 ? idx : AAC_SAMPLE_RATES.indexOf(16e3);
656
+ }
657
+ /**
658
+ * True when `buf` already begins with an ADTS syncword (0xFFF) + MPEG layer 0.
659
+ * Raw AAC AUs never do; ADTS-framed frames always do. Lets the wrapper pass
660
+ * already-framed input through untouched.
661
+ */
662
+ function hasAdtsSync(buf) {
663
+ return buf.length >= 2 && buf[0] === 255 && (buf[1] & 246) === 240;
664
+ }
665
+ /**
666
+ * Build the 7-byte ADTS header (no CRC) for a payload of `payloadLength` bytes.
667
+ */
668
+ function buildAdtsHeader(config, payloadLength) {
669
+ const objectType = config.aacObjectType ?? DEFAULT_AAC_OBJECT_TYPE;
670
+ const profile = Math.max(0, objectType - 1) & 3;
671
+ const freqIdx = aacSampleRateIndex(config.sampleRate) & 15;
672
+ const chanCfg = Math.max(1, config.channels) & 7;
673
+ const frameLength = payloadLength + 7;
674
+ const h = Buffer.alloc(7);
675
+ h[0] = 255;
676
+ h[1] = 241;
677
+ h[2] = profile << 6 | freqIdx << 2 | chanCfg >> 2 & 1;
678
+ h[3] = (chanCfg & 3) << 6 | frameLength >> 11 & 3;
679
+ h[4] = frameLength >> 3 & 255;
680
+ h[5] = (frameLength & 7) << 5 | 31;
681
+ h[6] = 252;
682
+ return h;
683
+ }
684
+ /**
685
+ * Return `frame` framed as ADTS: unchanged when it already carries an ADTS
686
+ * sync, otherwise the 7-byte header prepended. Pure; the input is never mutated.
687
+ */
688
+ function wrapAacAsAdts(frame, config) {
689
+ if (hasAdtsSync(frame)) return Buffer.from(frame.buffer, frame.byteOffset, frame.byteLength);
690
+ const payload = Buffer.from(frame.buffer, frame.byteOffset, frame.byteLength);
691
+ return Buffer.concat([buildAdtsHeader(config, payload.length), payload]);
692
+ }
693
+ //#endregion
618
694
  //#region src/audio-codec-ffmpeg/ffmpeg-audio-decode-session.ts
619
695
  var FfmpegAudioDecodeSession = class {
620
696
  logger;
@@ -632,6 +708,14 @@ var FfmpegAudioDecodeSession = class {
632
708
  * which pipe their encoded frames straight through.
633
709
  */
634
710
  oggFramer;
711
+ /**
712
+ * ADTS framing config, present ONLY when the codec is AAC. `ffmpeg -f aac`
713
+ * is the ADTS demuxer, but every AAC source delivers raw AAC access units
714
+ * (push sources emit bare frames; rfc3640 depacketization yields bare AUs) —
715
+ * piping those in raw fails `Invalid data found when processing input` → no
716
+ * PCM → silent WebRTC audio. Each frame is ADTS-wrapped before ffmpeg.
717
+ */
718
+ adtsConfig;
635
719
  residual = Buffer.alloc(0);
636
720
  nextPts = 0;
637
721
  destroyed = false;
@@ -646,6 +730,10 @@ var FfmpegAudioDecodeSession = class {
646
730
  channelCount: Math.max(1, params.sourceChannels),
647
731
  serial: serialFromSeed(params.oggSerialSeed ?? "audio-codec-ffmpeg-opus")
648
732
  }) : null;
733
+ this.adtsConfig = resolveAudioCodecAlias(params.codec) === "aac" ? {
734
+ sampleRate: params.sourceSampleRate,
735
+ channels: Math.max(1, params.sourceChannels)
736
+ } : null;
649
737
  const args = buildAudioDecodeArgs(params);
650
738
  this.process = new FfmpegAudioProcess({
651
739
  ffmpegPath: params.ffmpegPath,
@@ -669,6 +757,10 @@ var FfmpegAudioDecodeSession = class {
669
757
  this.process.write(this.oggFramer.framePacket(packet));
670
758
  return;
671
759
  }
760
+ if (this.adtsConfig) {
761
+ this.process.write(wrapAacAsAdts(data, this.adtsConfig));
762
+ return;
763
+ }
672
764
  this.process.write(data);
673
765
  }
674
766
  handleStdout(chunk) {
@@ -895,6 +987,29 @@ var AudioCodecFfmpegAddon = class extends require_dist.BaseAddon {
895
987
  if (!entry) return false;
896
988
  return input.kind === "decode" ? entry.canDecode : entry.canEncode;
897
989
  }
990
+ /**
991
+ * The BARE cluster node id this addon runs on (e.g. `hub`) — stamped into a
992
+ * session's return so the caller pins its follow-up `pushEncodedFrame` /
993
+ * `pullPcm` / `closeSession` to the same node. `ctx.kernel.localNodeId` is
994
+ * the COMPOUND `<node>/<addon>` form (e.g. `hub/audio-codec-ffmpeg`), but the
995
+ * cap route resolver matches an explicit `nodeId` against the bare node id
996
+ * (`classifyExplicitNode`: `nodeId === hubNodeId`). Returning the compound
997
+ * form makes every pinned call fall through to a remote-Moleculer dispatch
998
+ * for a non-existent service → `Services waiting is timed out` → no WebRTC
999
+ * audio, cluster-wide. Strip the addon suffix. Mirrors `decoder-ffmpeg`'s
1000
+ * `resolveLocalNodeId`.
1001
+ *
1002
+ * TODO(arch): this bare-node normalization is duplicated across
1003
+ * `decoder-ffmpeg`, `decoder-nodeav`, `audio-codec-ffmpeg`, and
1004
+ * `decoder-backend-keys.normalizeDecoderNodeId`. It should live ONCE on the
1005
+ * shared surface — either a bare `ctx.kernel.nodeId` accessor alongside the
1006
+ * compound `localNodeId`, or a base-addon helper — so no addon can leak the
1007
+ * compound `<node>/<addon>` id into a routing `nodeId` again (this exact bug).
1008
+ */
1009
+ resolveLocalNodeId() {
1010
+ const raw = this.ctx.kernel.localNodeId ?? "local";
1011
+ return raw.includes("/") ? raw.split("/")[0] : raw;
1012
+ }
898
1013
  async createDecodeSession(input) {
899
1014
  const codec = resolveAudioCodecAlias(input.codec);
900
1015
  const entry = CODEC_CATALOG.find((e) => resolveAudioCodecAlias(e.codec) === codec);
@@ -926,7 +1041,7 @@ var AudioCodecFfmpegAddon = class extends require_dist.BaseAddon {
926
1041
  });
927
1042
  return {
928
1043
  sessionId,
929
- nodeId: this.ctx.kernel.localNodeId ?? "local"
1044
+ nodeId: this.resolveLocalNodeId()
930
1045
  };
931
1046
  }
932
1047
  async createEncodeSession(input) {
@@ -960,7 +1075,7 @@ var AudioCodecFfmpegAddon = class extends require_dist.BaseAddon {
960
1075
  });
961
1076
  return {
962
1077
  sessionId,
963
- nodeId: this.ctx.kernel.localNodeId ?? "local"
1078
+ nodeId: this.resolveLocalNodeId()
964
1079
  };
965
1080
  }
966
1081
  async closeSession(input) {
@@ -1,4 +1,4 @@
1
- import { B as errMsg, V as BaseAddon, v as audioCodecCapability } from "../dist-DjuGmyG9.mjs";
1
+ import { B as errMsg, V as BaseAddon, v as audioCodecCapability } from "../dist-CfSORkv2.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region src/audio-codec-ffmpeg/ffmpeg-audio-process.ts
@@ -611,6 +611,82 @@ var OggOpusFramer = class {
611
611
  }
612
612
  };
613
613
  //#endregion
614
+ //#region src/audio-codec-ffmpeg/adts.ts
615
+ /**
616
+ * ADTS framing for raw AAC access units.
617
+ *
618
+ * The audio decode path runs `ffmpeg -f aac -i pipe:0` — the **ADTS** demuxer,
619
+ * which requires each frame to begin with a 7-byte ADTS header (0xFFF sync).
620
+ * But every AAC source that reaches the decoder delivers **raw** AAC access
621
+ * units with no ADTS header: push sources (Reolink Baichuan) emit one bare
622
+ * codec frame per packet, and rfc3640 (RTP AAC) depacketization yields bare
623
+ * AUs too. Piping those straight into `-f aac` fails with
624
+ * `Invalid data found when processing input` → no PCM → a silent WebRTC audio
625
+ * track. Wrapping each raw AU in an ADTS header (built from the source
626
+ * sample-rate / channel-count / AAC object type) makes the demuxer accept it.
627
+ *
628
+ * Opus is handled separately (Ogg encapsulation); this module is AAC-only.
629
+ */
630
+ /** MPEG-4 AAC sampling-frequency index table (ISO/IEC 14496-3). */
631
+ var AAC_SAMPLE_RATES = [
632
+ 96e3,
633
+ 88200,
634
+ 64e3,
635
+ 48e3,
636
+ 44100,
637
+ 32e3,
638
+ 24e3,
639
+ 22050,
640
+ 16e3,
641
+ 12e3,
642
+ 11025,
643
+ 8e3,
644
+ 7350
645
+ ];
646
+ /** AAC-LC is object type 2; the ADTS `profile` field is objectType - 1. */
647
+ var DEFAULT_AAC_OBJECT_TYPE = 2;
648
+ /** Resolve the ADTS sampling-frequency index for a sample rate (default 16 kHz → 8). */
649
+ function aacSampleRateIndex(sampleRate) {
650
+ const idx = AAC_SAMPLE_RATES.indexOf(sampleRate);
651
+ return idx >= 0 ? idx : AAC_SAMPLE_RATES.indexOf(16e3);
652
+ }
653
+ /**
654
+ * True when `buf` already begins with an ADTS syncword (0xFFF) + MPEG layer 0.
655
+ * Raw AAC AUs never do; ADTS-framed frames always do. Lets the wrapper pass
656
+ * already-framed input through untouched.
657
+ */
658
+ function hasAdtsSync(buf) {
659
+ return buf.length >= 2 && buf[0] === 255 && (buf[1] & 246) === 240;
660
+ }
661
+ /**
662
+ * Build the 7-byte ADTS header (no CRC) for a payload of `payloadLength` bytes.
663
+ */
664
+ function buildAdtsHeader(config, payloadLength) {
665
+ const objectType = config.aacObjectType ?? DEFAULT_AAC_OBJECT_TYPE;
666
+ const profile = Math.max(0, objectType - 1) & 3;
667
+ const freqIdx = aacSampleRateIndex(config.sampleRate) & 15;
668
+ const chanCfg = Math.max(1, config.channels) & 7;
669
+ const frameLength = payloadLength + 7;
670
+ const h = Buffer.alloc(7);
671
+ h[0] = 255;
672
+ h[1] = 241;
673
+ h[2] = profile << 6 | freqIdx << 2 | chanCfg >> 2 & 1;
674
+ h[3] = (chanCfg & 3) << 6 | frameLength >> 11 & 3;
675
+ h[4] = frameLength >> 3 & 255;
676
+ h[5] = (frameLength & 7) << 5 | 31;
677
+ h[6] = 252;
678
+ return h;
679
+ }
680
+ /**
681
+ * Return `frame` framed as ADTS: unchanged when it already carries an ADTS
682
+ * sync, otherwise the 7-byte header prepended. Pure; the input is never mutated.
683
+ */
684
+ function wrapAacAsAdts(frame, config) {
685
+ if (hasAdtsSync(frame)) return Buffer.from(frame.buffer, frame.byteOffset, frame.byteLength);
686
+ const payload = Buffer.from(frame.buffer, frame.byteOffset, frame.byteLength);
687
+ return Buffer.concat([buildAdtsHeader(config, payload.length), payload]);
688
+ }
689
+ //#endregion
614
690
  //#region src/audio-codec-ffmpeg/ffmpeg-audio-decode-session.ts
615
691
  var FfmpegAudioDecodeSession = class {
616
692
  logger;
@@ -628,6 +704,14 @@ var FfmpegAudioDecodeSession = class {
628
704
  * which pipe their encoded frames straight through.
629
705
  */
630
706
  oggFramer;
707
+ /**
708
+ * ADTS framing config, present ONLY when the codec is AAC. `ffmpeg -f aac`
709
+ * is the ADTS demuxer, but every AAC source delivers raw AAC access units
710
+ * (push sources emit bare frames; rfc3640 depacketization yields bare AUs) —
711
+ * piping those in raw fails `Invalid data found when processing input` → no
712
+ * PCM → silent WebRTC audio. Each frame is ADTS-wrapped before ffmpeg.
713
+ */
714
+ adtsConfig;
631
715
  residual = Buffer.alloc(0);
632
716
  nextPts = 0;
633
717
  destroyed = false;
@@ -642,6 +726,10 @@ var FfmpegAudioDecodeSession = class {
642
726
  channelCount: Math.max(1, params.sourceChannels),
643
727
  serial: serialFromSeed(params.oggSerialSeed ?? "audio-codec-ffmpeg-opus")
644
728
  }) : null;
729
+ this.adtsConfig = resolveAudioCodecAlias(params.codec) === "aac" ? {
730
+ sampleRate: params.sourceSampleRate,
731
+ channels: Math.max(1, params.sourceChannels)
732
+ } : null;
645
733
  const args = buildAudioDecodeArgs(params);
646
734
  this.process = new FfmpegAudioProcess({
647
735
  ffmpegPath: params.ffmpegPath,
@@ -665,6 +753,10 @@ var FfmpegAudioDecodeSession = class {
665
753
  this.process.write(this.oggFramer.framePacket(packet));
666
754
  return;
667
755
  }
756
+ if (this.adtsConfig) {
757
+ this.process.write(wrapAacAsAdts(data, this.adtsConfig));
758
+ return;
759
+ }
668
760
  this.process.write(data);
669
761
  }
670
762
  handleStdout(chunk) {
@@ -891,6 +983,29 @@ var AudioCodecFfmpegAddon = class extends BaseAddon {
891
983
  if (!entry) return false;
892
984
  return input.kind === "decode" ? entry.canDecode : entry.canEncode;
893
985
  }
986
+ /**
987
+ * The BARE cluster node id this addon runs on (e.g. `hub`) — stamped into a
988
+ * session's return so the caller pins its follow-up `pushEncodedFrame` /
989
+ * `pullPcm` / `closeSession` to the same node. `ctx.kernel.localNodeId` is
990
+ * the COMPOUND `<node>/<addon>` form (e.g. `hub/audio-codec-ffmpeg`), but the
991
+ * cap route resolver matches an explicit `nodeId` against the bare node id
992
+ * (`classifyExplicitNode`: `nodeId === hubNodeId`). Returning the compound
993
+ * form makes every pinned call fall through to a remote-Moleculer dispatch
994
+ * for a non-existent service → `Services waiting is timed out` → no WebRTC
995
+ * audio, cluster-wide. Strip the addon suffix. Mirrors `decoder-ffmpeg`'s
996
+ * `resolveLocalNodeId`.
997
+ *
998
+ * TODO(arch): this bare-node normalization is duplicated across
999
+ * `decoder-ffmpeg`, `decoder-nodeav`, `audio-codec-ffmpeg`, and
1000
+ * `decoder-backend-keys.normalizeDecoderNodeId`. It should live ONCE on the
1001
+ * shared surface — either a bare `ctx.kernel.nodeId` accessor alongside the
1002
+ * compound `localNodeId`, or a base-addon helper — so no addon can leak the
1003
+ * compound `<node>/<addon>` id into a routing `nodeId` again (this exact bug).
1004
+ */
1005
+ resolveLocalNodeId() {
1006
+ const raw = this.ctx.kernel.localNodeId ?? "local";
1007
+ return raw.includes("/") ? raw.split("/")[0] : raw;
1008
+ }
894
1009
  async createDecodeSession(input) {
895
1010
  const codec = resolveAudioCodecAlias(input.codec);
896
1011
  const entry = CODEC_CATALOG.find((e) => resolveAudioCodecAlias(e.codec) === codec);
@@ -922,7 +1037,7 @@ var AudioCodecFfmpegAddon = class extends BaseAddon {
922
1037
  });
923
1038
  return {
924
1039
  sessionId,
925
- nodeId: this.ctx.kernel.localNodeId ?? "local"
1040
+ nodeId: this.resolveLocalNodeId()
926
1041
  };
927
1042
  }
928
1043
  async createEncodeSession(input) {
@@ -956,7 +1071,7 @@ var AudioCodecFfmpegAddon = class extends BaseAddon {
956
1071
  });
957
1072
  return {
958
1073
  sessionId,
959
- nodeId: this.ctx.kernel.localNodeId ?? "local"
1074
+ nodeId: this.resolveLocalNodeId()
960
1075
  };
961
1076
  }
962
1077
  async closeSession(input) {
@@ -2,9 +2,9 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-Cwc0TUQr.js");
6
- const require_frame_ring_sink = require("../frame-ring-sink-DvdhCFr3.js");
7
- const require_ffmpeg_args = require("../ffmpeg-args-D6h1edXK.js");
5
+ const require_dist = require("../dist-BrIE85BG.js");
6
+ const require_frame_ring_sink = require("../frame-ring-sink-B-DxHelU.js");
7
+ const require_ffmpeg_args = require("../ffmpeg-args-Dl9mLcOC.js");
8
8
  const require_frame_dropper = require("../frame-dropper-DKLM6pMz.js");
9
9
  let _camstack_shm_ring = require("@camstack/shm-ring");
10
10
  let node_crypto = require("node:crypto");
@@ -114,6 +114,13 @@ var HWACCEL_FALLBACK_WINDOW_MS = 4e3;
114
114
  * session down (`destroy`) cancels the pending re-dial.
115
115
  */
116
116
  var PULL_REDIAL_DELAY_MS = 3e3;
117
+ /**
118
+ * Safety ceiling for the current-GOP replay buffer (push mode). A GOP resets on
119
+ * every keyframe so this is never reached by a well-behaved stream; it only
120
+ * bounds a pathological long/no-keyframe stream. When exceeded, only the
121
+ * keyframe is retained — enough to re-prime the decoder's SPS/PPS on a re-spawn.
122
+ */
123
+ var MAX_GOP_BUFFER_BYTES = 16 * 1024 * 1024;
117
124
  var FfmpegDecoderSession = class {
118
125
  config;
119
126
  logger;
@@ -185,6 +192,18 @@ var FfmpegDecoderSession = class {
185
192
  handleCallbacks = /* @__PURE__ */ new Set();
186
193
  frameDropper;
187
194
  inputPackets = 0;
195
+ /**
196
+ * Current-GOP Annex-B packet bytes (push mode), from the most recent keyframe
197
+ * forward. Replayed keyframe-first into every freshly-spawned child so a
198
+ * hwaccel cascade re-spawn (`vaapi → qsv → software`) or a config restart
199
+ * resumes on a keyframe instead of the live mid-GOP P-frame the dead child had
200
+ * already consumed — which the fallback child would otherwise choke on
201
+ * (`non-existing PPS 0`) until the next live keyframe. Mirrors the reference
202
+ * NVR libav decoder, which replays its buffered packet list to each hwaccel
203
+ * candidate. Reset on each keyframe; unused in pull mode.
204
+ */
205
+ gopBuffer = [];
206
+ gopBufferBytes = 0;
188
207
  outputFrames = 0;
189
208
  droppedFrames = 0;
190
209
  startTime = Date.now();
@@ -280,6 +299,7 @@ var FfmpegDecoderSession = class {
280
299
  this.process = child;
281
300
  this.spawnedAtMs = Date.now();
282
301
  child.stdin?.on("error", () => {});
302
+ this.replayGopBuffer(child);
283
303
  child.stdout?.on("data", (chunk) => {
284
304
  if (child === this.process) this.handleStdout(chunk);
285
305
  });
@@ -491,14 +511,50 @@ var FfmpegDecoderSession = class {
491
511
  pushPacket(packet) {
492
512
  if (this.destroyed) return;
493
513
  if (this.pullUrl !== null) return;
494
- if (this.process === null && !this.spawnAttempted) this.spawnFfmpeg();
514
+ this.recordGop(packet);
515
+ this.inputPackets++;
516
+ if (this.process === null && !this.spawnAttempted) {
517
+ this.spawnFfmpeg();
518
+ return;
519
+ }
495
520
  const stdin = this.process?.stdin;
496
521
  if (!stdin) return;
497
- this.inputPackets++;
498
522
  try {
499
523
  stdin.write(packet.data);
500
524
  } catch {}
501
525
  }
526
+ /**
527
+ * Accumulate the current GOP for the keyframe-first re-spawn replay. Reset on
528
+ * each keyframe; P-frames before the first keyframe are ignored (useless to
529
+ * prime a decoder). A pathological long/no-keyframe GOP is capped to the
530
+ * keyframe alone (enough to re-prime SPS/PPS).
531
+ */
532
+ recordGop(packet) {
533
+ if (packet.keyframe) {
534
+ this.gopBuffer = [packet.data];
535
+ this.gopBufferBytes = packet.data.length;
536
+ return;
537
+ }
538
+ if (this.gopBuffer.length === 0) return;
539
+ this.gopBuffer.push(packet.data);
540
+ this.gopBufferBytes += packet.data.length;
541
+ if (this.gopBufferBytes > MAX_GOP_BUFFER_BYTES) {
542
+ const keyframe = this.gopBuffer[0];
543
+ this.gopBuffer = [keyframe];
544
+ this.gopBufferBytes = keyframe.length;
545
+ }
546
+ }
547
+ /** Replay the buffered GOP (keyframe-first) into a freshly-spawned push child. */
548
+ replayGopBuffer(child) {
549
+ if (this.pullUrl !== null || this.gopBuffer.length === 0) return;
550
+ const stdin = child.stdin;
551
+ if (!stdin) return;
552
+ for (const buf of this.gopBuffer) try {
553
+ stdin.write(buf);
554
+ } catch {
555
+ return;
556
+ }
557
+ }
502
558
  onFrame(callback) {
503
559
  this.frameCallbacks.add(callback);
504
560
  return () => {
@@ -709,7 +765,16 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
709
765
  if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
710
766
  this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
711
767
  });
712
- const backend = await require_frame_ring_sink.resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
768
+ let backend;
769
+ try {
770
+ backend = require_frame_ring_sink.resolveOwnDecoderBackend(await this.resolveGlobalStore(this.resolveLocalNodeId()));
771
+ } catch (err) {
772
+ this.ctx.logger.warn("decoder-ffmpeg: own-store backend read failed — using default", { meta: {
773
+ default: require_frame_ring_sink.DEFAULT_DECODER_BACKEND,
774
+ error: err instanceof Error ? err.message : String(err)
775
+ } });
776
+ backend = require_frame_ring_sink.DEFAULT_DECODER_BACKEND;
777
+ }
713
778
  if (backend !== "ffmpeg") {
714
779
  this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
715
780
  return [];
@@ -1,6 +1,6 @@
1
- import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-DjuGmyG9.mjs";
2
- import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, s as DEFAULT_DECODER_BACKEND, t as DecoderFrameRingSink } from "../frame-ring-sink-sY-8XUsH.mjs";
3
- import { a as codecToInputFormat, c as maxDecodeWidth, d as pickDecodeHwAccel, f as rankDecodeHwAccels, i as channelsForPixel, l as parseAvailableGpuScaleFilters, m as resolveGpuScaleFilter, n as DEFAULT_VAAPI_RENDER_DEVICE, o as computeOutputGeometry, p as rawPixelForFormat, r as buildFfmpegDecodeArgs, s as isSoftwareHwAccel, t as DECODE_HWACCEL_RANK, u as parseFfmpegOutputDims } from "../ffmpeg-args-C5GPp8Cw.mjs";
1
+ import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-CfSORkv2.mjs";
2
+ import { c as DEFAULT_DECODER_BACKEND, i as NotifyingRingBuffer, n as RING_BUDGET_MB, s as resolveOwnDecoderBackend, t as DecoderFrameRingSink } from "../frame-ring-sink-B_WjDX0x.mjs";
3
+ import { a as codecToInputFormat, c as maxDecodeWidth, d as pickDecodeHwAccel, f as rankDecodeHwAccels, i as channelsForPixel, l as parseAvailableGpuScaleFilters, m as resolveGpuScaleFilter, n as DEFAULT_VAAPI_RENDER_DEVICE, o as computeOutputGeometry, p as rawPixelForFormat, r as buildFfmpegDecodeArgs, s as isSoftwareHwAccel, t as DECODE_HWACCEL_RANK, u as parseFfmpegOutputDims } from "../ffmpeg-args-CpeRWwL4.mjs";
4
4
  import { t as FrameDropper } from "../frame-dropper-AjheBGMG.mjs";
5
5
  import { FrameRingReaderCache } from "@camstack/shm-ring";
6
6
  import { randomUUID } from "node:crypto";
@@ -110,6 +110,13 @@ var HWACCEL_FALLBACK_WINDOW_MS = 4e3;
110
110
  * session down (`destroy`) cancels the pending re-dial.
111
111
  */
112
112
  var PULL_REDIAL_DELAY_MS = 3e3;
113
+ /**
114
+ * Safety ceiling for the current-GOP replay buffer (push mode). A GOP resets on
115
+ * every keyframe so this is never reached by a well-behaved stream; it only
116
+ * bounds a pathological long/no-keyframe stream. When exceeded, only the
117
+ * keyframe is retained — enough to re-prime the decoder's SPS/PPS on a re-spawn.
118
+ */
119
+ var MAX_GOP_BUFFER_BYTES = 16 * 1024 * 1024;
113
120
  var FfmpegDecoderSession = class {
114
121
  config;
115
122
  logger;
@@ -181,6 +188,18 @@ var FfmpegDecoderSession = class {
181
188
  handleCallbacks = /* @__PURE__ */ new Set();
182
189
  frameDropper;
183
190
  inputPackets = 0;
191
+ /**
192
+ * Current-GOP Annex-B packet bytes (push mode), from the most recent keyframe
193
+ * forward. Replayed keyframe-first into every freshly-spawned child so a
194
+ * hwaccel cascade re-spawn (`vaapi → qsv → software`) or a config restart
195
+ * resumes on a keyframe instead of the live mid-GOP P-frame the dead child had
196
+ * already consumed — which the fallback child would otherwise choke on
197
+ * (`non-existing PPS 0`) until the next live keyframe. Mirrors the reference
198
+ * NVR libav decoder, which replays its buffered packet list to each hwaccel
199
+ * candidate. Reset on each keyframe; unused in pull mode.
200
+ */
201
+ gopBuffer = [];
202
+ gopBufferBytes = 0;
184
203
  outputFrames = 0;
185
204
  droppedFrames = 0;
186
205
  startTime = Date.now();
@@ -276,6 +295,7 @@ var FfmpegDecoderSession = class {
276
295
  this.process = child;
277
296
  this.spawnedAtMs = Date.now();
278
297
  child.stdin?.on("error", () => {});
298
+ this.replayGopBuffer(child);
279
299
  child.stdout?.on("data", (chunk) => {
280
300
  if (child === this.process) this.handleStdout(chunk);
281
301
  });
@@ -487,14 +507,50 @@ var FfmpegDecoderSession = class {
487
507
  pushPacket(packet) {
488
508
  if (this.destroyed) return;
489
509
  if (this.pullUrl !== null) return;
490
- if (this.process === null && !this.spawnAttempted) this.spawnFfmpeg();
510
+ this.recordGop(packet);
511
+ this.inputPackets++;
512
+ if (this.process === null && !this.spawnAttempted) {
513
+ this.spawnFfmpeg();
514
+ return;
515
+ }
491
516
  const stdin = this.process?.stdin;
492
517
  if (!stdin) return;
493
- this.inputPackets++;
494
518
  try {
495
519
  stdin.write(packet.data);
496
520
  } catch {}
497
521
  }
522
+ /**
523
+ * Accumulate the current GOP for the keyframe-first re-spawn replay. Reset on
524
+ * each keyframe; P-frames before the first keyframe are ignored (useless to
525
+ * prime a decoder). A pathological long/no-keyframe GOP is capped to the
526
+ * keyframe alone (enough to re-prime SPS/PPS).
527
+ */
528
+ recordGop(packet) {
529
+ if (packet.keyframe) {
530
+ this.gopBuffer = [packet.data];
531
+ this.gopBufferBytes = packet.data.length;
532
+ return;
533
+ }
534
+ if (this.gopBuffer.length === 0) return;
535
+ this.gopBuffer.push(packet.data);
536
+ this.gopBufferBytes += packet.data.length;
537
+ if (this.gopBufferBytes > MAX_GOP_BUFFER_BYTES) {
538
+ const keyframe = this.gopBuffer[0];
539
+ this.gopBuffer = [keyframe];
540
+ this.gopBufferBytes = keyframe.length;
541
+ }
542
+ }
543
+ /** Replay the buffered GOP (keyframe-first) into a freshly-spawned push child. */
544
+ replayGopBuffer(child) {
545
+ if (this.pullUrl !== null || this.gopBuffer.length === 0) return;
546
+ const stdin = child.stdin;
547
+ if (!stdin) return;
548
+ for (const buf of this.gopBuffer) try {
549
+ stdin.write(buf);
550
+ } catch {
551
+ return;
552
+ }
553
+ }
498
554
  onFrame(callback) {
499
555
  this.frameCallbacks.add(callback);
500
556
  return () => {
@@ -705,7 +761,16 @@ var DecoderFfmpegAddon = class extends BaseAddon {
705
761
  if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
706
762
  this.ctx.logger.warn("ffmpeg: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
707
763
  });
708
- const backend = await resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
764
+ let backend;
765
+ try {
766
+ backend = resolveOwnDecoderBackend(await this.resolveGlobalStore(this.resolveLocalNodeId()));
767
+ } catch (err) {
768
+ this.ctx.logger.warn("decoder-ffmpeg: own-store backend read failed — using default", { meta: {
769
+ default: DEFAULT_DECODER_BACKEND,
770
+ error: err instanceof Error ? err.message : String(err)
771
+ } });
772
+ backend = DEFAULT_DECODER_BACKEND;
773
+ }
709
774
  if (backend !== "ffmpeg") {
710
775
  this.ctx.logger.info("ffmpeg decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
711
776
  return [];