@camstack/addon-decoder-nodeav 1.1.2 → 1.1.4

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 (3) hide show
  1. package/dist/index.js +891 -39
  2. package/dist/index.mjs +887 -40
  3. package/package.json +6 -2
package/dist/index.js CHANGED
@@ -12797,42 +12797,79 @@ var SessionInventoryEntrySchema = object({
12797
12797
  framesIn: number(),
12798
12798
  framesOut: number()
12799
12799
  });
12800
- method(_void(), array(AudioCodecInfoSchema).readonly()), method(object({
12801
- codec: string(),
12802
- kind: _enum(["decode", "encode"])
12803
- }), boolean()), method(AudioDecodeSessionConfigSchema, object({
12804
- sessionId: string(),
12805
- nodeId: string()
12806
- }), { kind: "mutation" }), method(AudioEncodeSessionConfigSchema, object({
12807
- sessionId: string(),
12808
- nodeId: string()
12809
- }), { kind: "mutation" }), method(object({
12810
- sessionId: string(),
12811
- nodeId: string().optional()
12812
- }), _void(), { kind: "mutation" }), method(object({
12813
- sessionId: string(),
12814
- nodeId: string().optional(),
12815
- data: _instanceof(Uint8Array),
12816
- /** Source PTS in milliseconds. Synthesised when omitted. */
12817
- pts: number().optional()
12818
- }), _void(), { kind: "mutation" }), method(object({
12819
- sessionId: string(),
12820
- nodeId: string().optional(),
12821
- maxCount: number().int().positive().default(8)
12822
- }), array(AudioPcmChunkSchema)), method(object({
12823
- sessionId: string(),
12824
- nodeId: string().optional(),
12825
- data: _instanceof(Uint8Array),
12826
- /** Source PTS in milliseconds. */
12827
- pts: number().optional()
12828
- }), _void(), { kind: "mutation" }), method(object({
12829
- sessionId: string(),
12830
- nodeId: string().optional(),
12831
- maxCount: number().int().positive().default(8)
12832
- }), array(AudioEncodedChunkSchema)), method(object({
12833
- sessionId: string(),
12834
- nodeId: string().optional()
12835
- }), array(AudioEncodedChunkSchema), { kind: "mutation" }), method(_void(), array(SessionInventoryEntrySchema).readonly());
12800
+ /**
12801
+ * audio-codec — bidirectional PCM ↔ encoded audio I/O box.
12802
+ *
12803
+ * Independent per-consumer sessions. The provider runs decode + resample
12804
+ * (or resample + encode) inside the session so a 16kHz mono ASA
12805
+ * subscriber and a 48kHz stereo WebRTC subscriber on the same source
12806
+ * stream don't share resamplers.
12807
+ *
12808
+ * Singleton on each node. Decoder and encoder live in the same provider
12809
+ * because they share the underlying libav contexts (node-av today,
12810
+ * pluggable later) — operators always install one or the other together.
12811
+ */
12812
+ var audioCodecCapability = {
12813
+ name: "audio-codec",
12814
+ scope: "system",
12815
+ mode: "singleton",
12816
+ preferredProvider: "decoder-nodeav",
12817
+ methods: {
12818
+ /** Probe the local runtime and return the supported codec matrix. */
12819
+ listSupportedCodecs: method(_void(), array(AudioCodecInfoSchema).readonly()),
12820
+ /** Cheap predicate — does the runtime support `(codec, kind)`? */
12821
+ canHandle: method(object({
12822
+ codec: string(),
12823
+ kind: _enum(["decode", "encode"])
12824
+ }), boolean()),
12825
+ createDecodeSession: method(AudioDecodeSessionConfigSchema, object({
12826
+ sessionId: string(),
12827
+ nodeId: string()
12828
+ }), { kind: "mutation" }),
12829
+ createEncodeSession: method(AudioEncodeSessionConfigSchema, object({
12830
+ sessionId: string(),
12831
+ nodeId: string()
12832
+ }), { kind: "mutation" }),
12833
+ closeSession: method(object({
12834
+ sessionId: string(),
12835
+ nodeId: string().optional()
12836
+ }), _void(), { kind: "mutation" }),
12837
+ /** Push one encoded audio frame into a decode session. */
12838
+ pushEncodedFrame: method(object({
12839
+ sessionId: string(),
12840
+ nodeId: string().optional(),
12841
+ data: _instanceof(Uint8Array),
12842
+ /** Source PTS in milliseconds. Synthesised when omitted. */
12843
+ pts: number().optional()
12844
+ }), _void(), { kind: "mutation" }),
12845
+ /** Pull up to `maxCount` PCM chunks from a decode session. */
12846
+ pullPcm: method(object({
12847
+ sessionId: string(),
12848
+ nodeId: string().optional(),
12849
+ maxCount: number().int().positive().default(8)
12850
+ }), array(AudioPcmChunkSchema)),
12851
+ /** Push one PCM chunk into an encode session. */
12852
+ pushPcm: method(object({
12853
+ sessionId: string(),
12854
+ nodeId: string().optional(),
12855
+ data: _instanceof(Uint8Array),
12856
+ /** Source PTS in milliseconds. */
12857
+ pts: number().optional()
12858
+ }), _void(), { kind: "mutation" }),
12859
+ /** Pull up to `maxCount` encoded chunks from an encode session. */
12860
+ pullEncoded: method(object({
12861
+ sessionId: string(),
12862
+ nodeId: string().optional(),
12863
+ maxCount: number().int().positive().default(8)
12864
+ }), array(AudioEncodedChunkSchema)),
12865
+ /** Flush any pending encoded output (call before close on graceful tear). */
12866
+ flushEncode: method(object({
12867
+ sessionId: string(),
12868
+ nodeId: string().optional()
12869
+ }), array(AudioEncodedChunkSchema), { kind: "mutation" }),
12870
+ listActiveSessions: method(_void(), array(SessionInventoryEntrySchema).readonly())
12871
+ }
12872
+ };
12836
12873
  var AuthResultSchema = object({
12837
12874
  userId: string(),
12838
12875
  username: string(),
@@ -24300,6 +24337,788 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
24300
24337
  }
24301
24338
  };
24302
24339
  //#endregion
24340
+ //#region src/audio-codec/codec-catalog.ts
24341
+ /**
24342
+ * Normalise an SDP-reported codec name to the libav/ffmpeg name. Mirrors
24343
+ * `audio-codec-ffmpeg`'s `resolveAudioCodecAlias` so callers can pass the
24344
+ * SDP-reported value verbatim.
24345
+ */
24346
+ function resolveAudioCodecAlias(codec) {
24347
+ const c = codec.toLowerCase();
24348
+ if (c === "mpeg4-generic") return "aac";
24349
+ if (c === "l16") return "pcm_s16be";
24350
+ return c;
24351
+ }
24352
+ /**
24353
+ * Supported codec matrix. `aac_latm` / `mpeg4-generic` are decode-only (they
24354
+ * exist only as camera-side inbound streams; the intercom back-channel encodes
24355
+ * plain `aac`). Kept byte-for-byte in step with the ffmpeg addon's catalogue.
24356
+ */
24357
+ var CODEC_CATALOG = [
24358
+ {
24359
+ codec: "pcm_mulaw",
24360
+ canDecode: true,
24361
+ canEncode: true,
24362
+ label: "PCM µ-law (G.711)"
24363
+ },
24364
+ {
24365
+ codec: "pcm_alaw",
24366
+ canDecode: true,
24367
+ canEncode: true,
24368
+ label: "PCM A-law (G.711)"
24369
+ },
24370
+ {
24371
+ codec: "g722",
24372
+ canDecode: true,
24373
+ canEncode: true,
24374
+ label: "G.722"
24375
+ },
24376
+ {
24377
+ codec: "aac",
24378
+ canDecode: true,
24379
+ canEncode: true,
24380
+ label: "AAC"
24381
+ },
24382
+ {
24383
+ codec: "aac_latm",
24384
+ canDecode: true,
24385
+ canEncode: false,
24386
+ label: "AAC LATM"
24387
+ },
24388
+ {
24389
+ codec: "mpeg4-generic",
24390
+ canDecode: true,
24391
+ canEncode: false,
24392
+ label: "AAC (MPEG4-GENERIC)"
24393
+ },
24394
+ {
24395
+ codec: "opus",
24396
+ canDecode: true,
24397
+ canEncode: true,
24398
+ label: "Opus"
24399
+ }
24400
+ ];
24401
+ /** Resolve the catalogue entry for a codec name (alias-normalised). */
24402
+ function catalogEntryFor(codec) {
24403
+ const resolved = resolveAudioCodecAlias(codec);
24404
+ return CODEC_CATALOG.find((e) => resolveAudioCodecAlias(e.codec) === resolved) ?? null;
24405
+ }
24406
+ /** Cheap predicate — is `(codec, kind)` in the catalogue? */
24407
+ function codecSupports(codec, kind) {
24408
+ const entry = catalogEntryFor(codec);
24409
+ if (!entry) return false;
24410
+ return kind === "decode" ? entry.canDecode : entry.canEncode;
24411
+ }
24412
+ /**
24413
+ * Resolve an (alias-normalised) codec name to its canonical libav key, or
24414
+ * `null` for names outside this addon's SDP audio matrix. Kept pure (string
24415
+ * only) — the branded `AVCodecID` lookup lives in `nodeav-av-types.ts` where
24416
+ * node-av's constants are in scope. G.722 maps to `AV_CODEC_ID_ADPCM_G722`.
24417
+ */
24418
+ function resolveCodecKey(codec) {
24419
+ switch (resolveAudioCodecAlias(codec)) {
24420
+ case "aac": return "aac";
24421
+ case "aac_latm": return "aac_latm";
24422
+ case "opus": return "opus";
24423
+ case "pcm_alaw": return "pcm_alaw";
24424
+ case "pcm_mulaw": return "pcm_mulaw";
24425
+ case "g722": return "g722";
24426
+ default: return null;
24427
+ }
24428
+ }
24429
+ //#endregion
24430
+ //#region src/audio-codec/nodeav-av-types.ts
24431
+ /**
24432
+ * Resolve a codec name to its branded libav `AVCodecID` using the real
24433
+ * constants. Returns `null` for names outside this addon's matrix. G.722 maps
24434
+ * to `AV_CODEC_ID_ADPCM_G722` (there is no plain `AV_CODEC_ID_G722`).
24435
+ */
24436
+ function resolveAvCodecId(codec, consts) {
24437
+ switch (resolveCodecKey(codec)) {
24438
+ case "aac": return consts.AV_CODEC_ID_AAC;
24439
+ case "aac_latm": return consts.AV_CODEC_ID_AAC_LATM;
24440
+ case "opus": return consts.AV_CODEC_ID_OPUS;
24441
+ case "pcm_alaw": return consts.AV_CODEC_ID_PCM_ALAW;
24442
+ case "pcm_mulaw": return consts.AV_CODEC_ID_PCM_MULAW;
24443
+ case "g722": return consts.AV_CODEC_ID_ADPCM_G722;
24444
+ case null: return null;
24445
+ }
24446
+ }
24447
+ /** Build a canonical mono/stereo (or bare-mask) channel layout. */
24448
+ function buildChannelLayout(consts, channels) {
24449
+ if (channels === 1) return {
24450
+ nbChannels: 1,
24451
+ order: consts.AV_CHANNEL_ORDER_NATIVE,
24452
+ mask: consts.AV_CH_LAYOUT_MONO
24453
+ };
24454
+ if (channels === 2) return {
24455
+ nbChannels: 2,
24456
+ order: consts.AV_CHANNEL_ORDER_NATIVE,
24457
+ mask: consts.AV_CH_LAYOUT_STEREO
24458
+ };
24459
+ return {
24460
+ nbChannels: channels,
24461
+ order: consts.AV_CHANNEL_ORDER_NATIVE,
24462
+ mask: 0n
24463
+ };
24464
+ }
24465
+ //#endregion
24466
+ //#region src/audio-codec/nodeav-audio-decode-session.ts
24467
+ function targetSampleFmt(consts, format) {
24468
+ return format === "f32le" ? consts.AV_SAMPLE_FMT_FLT : consts.AV_SAMPLE_FMT_S16;
24469
+ }
24470
+ function bytesPerSample(format) {
24471
+ return format === "f32le" ? 4 : 2;
24472
+ }
24473
+ var NodeAvAudioDecodeSession = class {
24474
+ runtime;
24475
+ cfg;
24476
+ logger;
24477
+ onPcm;
24478
+ ctx;
24479
+ swr;
24480
+ packet;
24481
+ inFrame;
24482
+ outFrame;
24483
+ outLayout;
24484
+ outSampleFmt;
24485
+ outBytesPerSample;
24486
+ outFormat;
24487
+ nextPts = 0;
24488
+ closed = false;
24489
+ /**
24490
+ * Synchronously build a decode runtime from the already-loaded node-av
24491
+ * runtime. Throws (freeing anything already allocated) on any libav error so
24492
+ * the addon surfaces a clean "decode session failed" without leaking a
24493
+ * half-open context.
24494
+ */
24495
+ constructor(runtime, cfg, logger, onPcm) {
24496
+ this.runtime = runtime;
24497
+ this.cfg = cfg;
24498
+ this.logger = logger;
24499
+ this.onPcm = onPcm;
24500
+ this.outFormat = cfg.targetFormat;
24501
+ this.outBytesPerSample = bytesPerSample(cfg.targetFormat);
24502
+ const { nav, consts } = runtime;
24503
+ const codecId = resolveAvCodecId(cfg.codec, consts);
24504
+ if (codecId === null) throw new Error(`audio-codec-nodeav: unknown codec '${cfg.codec}' for decode`);
24505
+ const codec = nav.Codec.findDecoder(codecId);
24506
+ if (!codec) throw new Error(`audio-codec-nodeav: decoder not registered for '${cfg.codec}'`);
24507
+ const ctx = new nav.CodecContext();
24508
+ ctx.allocContext3(codec);
24509
+ ctx.sampleRate = cfg.sourceSampleRate;
24510
+ const inLayout = buildChannelLayout(consts, cfg.sourceChannels);
24511
+ ctx.channelLayout = inLayout;
24512
+ if (cfg.extraData && cfg.extraData.byteLength > 0) ctx.extraData = Buffer.from(cfg.extraData);
24513
+ const openRet = ctx.open2Sync(codec, null);
24514
+ if (openRet < 0) {
24515
+ ctx.freeContext();
24516
+ throw new Error(`audio-codec-nodeav: open2 failed for '${cfg.codec}' (ret=${openRet})`);
24517
+ }
24518
+ const inSampleFmt = ctx.sampleFormat;
24519
+ this.outSampleFmt = targetSampleFmt(consts, cfg.targetFormat);
24520
+ this.outLayout = buildChannelLayout(consts, cfg.targetChannels);
24521
+ const swr = new nav.SoftwareResampleContext();
24522
+ const allocRet = swr.allocSetOpts2(this.outLayout, this.outSampleFmt, cfg.targetSampleRate, inLayout, inSampleFmt, cfg.sourceSampleRate);
24523
+ if (allocRet < 0) {
24524
+ ctx.freeContext();
24525
+ throw new Error(`audio-codec-nodeav: swr allocSetOpts2 failed (ret=${allocRet})`);
24526
+ }
24527
+ const initRet = swr.init();
24528
+ if (initRet < 0) {
24529
+ swr.free();
24530
+ ctx.freeContext();
24531
+ throw new Error(`audio-codec-nodeav: swr init failed (ret=${initRet})`);
24532
+ }
24533
+ const packet = new nav.Packet();
24534
+ packet.alloc();
24535
+ const inFrame = new nav.Frame();
24536
+ inFrame.alloc();
24537
+ const outFrame = new nav.Frame();
24538
+ outFrame.alloc();
24539
+ this.ctx = ctx;
24540
+ this.swr = swr;
24541
+ this.packet = packet;
24542
+ this.inFrame = inFrame;
24543
+ this.outFrame = outFrame;
24544
+ }
24545
+ /** Decode one encoded access unit, emitting resampled PCM via `onPcm`. */
24546
+ pushEncoded(data, pts) {
24547
+ if (this.closed) return;
24548
+ const { consts } = this.runtime;
24549
+ this.packet.data = Buffer.from(data);
24550
+ if (pts !== void 0) {
24551
+ const p = BigInt(Math.round(pts));
24552
+ this.packet.pts = p;
24553
+ this.packet.dts = p;
24554
+ }
24555
+ const sendRet = this.ctx.sendPacketSync(this.packet);
24556
+ if (sendRet < 0 && sendRet !== consts.AVERROR_EAGAIN) throw new Error(`audio-codec-nodeav: sendPacket failed (ret=${sendRet})`);
24557
+ for (;;) {
24558
+ const recvRet = this.ctx.receiveFrameSync(this.inFrame);
24559
+ if (recvRet === consts.AVERROR_EAGAIN || recvRet === consts.AVERROR_EOF) break;
24560
+ if (recvRet < 0) throw new Error(`audio-codec-nodeav: receiveFrame failed (ret=${recvRet})`);
24561
+ try {
24562
+ const chunk = this.resampleFrame(this.inFrame);
24563
+ if (chunk) this.onPcm(chunk);
24564
+ } finally {
24565
+ this.inFrame.unref();
24566
+ }
24567
+ }
24568
+ }
24569
+ resampleFrame(inFrame) {
24570
+ const outSamples = this.swr.getOutSamples(inFrame.nbSamples);
24571
+ if (outSamples <= 0) return null;
24572
+ this.outFrame.unref();
24573
+ this.outFrame.format = this.outSampleFmt;
24574
+ this.outFrame.sampleRate = this.cfg.targetSampleRate;
24575
+ this.outFrame.channelLayout = this.outLayout;
24576
+ this.outFrame.nbSamples = outSamples;
24577
+ const bufRet = this.outFrame.getBuffer(0);
24578
+ if (bufRet < 0) throw new Error(`audio-codec-nodeav: outFrame.getBuffer failed (ret=${bufRet})`);
24579
+ const convRet = this.swr.convertFrame(this.outFrame, inFrame);
24580
+ if (convRet < 0) throw new Error(`audio-codec-nodeav: swr.convertFrame failed (ret=${convRet})`);
24581
+ const produced = this.outFrame.nbSamples;
24582
+ if (produced <= 0) return null;
24583
+ const planes = this.outFrame.extendedData;
24584
+ const plane0 = planes && planes.length > 0 ? planes[0] : null;
24585
+ if (!plane0) return null;
24586
+ const bytes = produced * this.cfg.targetChannels * this.outBytesPerSample;
24587
+ const out = new Uint8Array(new ArrayBuffer(bytes));
24588
+ out.set(plane0.subarray(0, bytes));
24589
+ const ptsMs = this.nextPts;
24590
+ this.nextPts = ptsMs + Math.round(produced * 1e3 / this.cfg.targetSampleRate);
24591
+ return {
24592
+ data: out,
24593
+ sampleRate: this.cfg.targetSampleRate,
24594
+ channels: this.cfg.targetChannels,
24595
+ format: this.outFormat,
24596
+ pts: ptsMs
24597
+ };
24598
+ }
24599
+ /** Release every native handle. Idempotent; safe to call from a reaper. */
24600
+ destroy() {
24601
+ if (this.closed) return;
24602
+ this.closed = true;
24603
+ this.freeQuietly(() => this.outFrame.free(), "outFrame");
24604
+ this.freeQuietly(() => this.inFrame.free(), "inFrame");
24605
+ this.freeQuietly(() => this.packet.free(), "packet");
24606
+ this.freeQuietly(() => this.swr.free(), "swr");
24607
+ this.freeQuietly(() => this.ctx.freeContext(), "codecContext");
24608
+ }
24609
+ freeQuietly(fn, what) {
24610
+ try {
24611
+ fn();
24612
+ } catch (err) {
24613
+ this.logger.warn("audio-codec-nodeav: decode handle free failed", { meta: {
24614
+ what,
24615
+ error: err instanceof Error ? err.message : String(err)
24616
+ } });
24617
+ }
24618
+ }
24619
+ };
24620
+ //#endregion
24621
+ //#region src/audio-codec/nodeav-audio-encode-session.ts
24622
+ function sourceSampleFmt(consts, format) {
24623
+ return format === "f32le" ? consts.AV_SAMPLE_FMT_FLT : consts.AV_SAMPLE_FMT_S16;
24624
+ }
24625
+ function sourceBytesPerSample(format) {
24626
+ return format === "f32le" ? 4 : 2;
24627
+ }
24628
+ var NodeAvAudioEncodeSession = class {
24629
+ runtime;
24630
+ cfg;
24631
+ logger;
24632
+ onChunk;
24633
+ codecName;
24634
+ ctx;
24635
+ swr;
24636
+ fifo;
24637
+ packet;
24638
+ outLayout;
24639
+ encoderSampleFmt;
24640
+ srcSampleFmt;
24641
+ srcBytesPerSample;
24642
+ /** Encoder-required samples per frame; `<= 0` means variable (drain all). */
24643
+ frameSize;
24644
+ ptsSamples = 0;
24645
+ closed = false;
24646
+ constructor(runtime, cfg, logger, onChunk) {
24647
+ this.runtime = runtime;
24648
+ this.cfg = cfg;
24649
+ this.logger = logger;
24650
+ this.onChunk = onChunk;
24651
+ this.codecName = resolveAudioCodecAlias(cfg.codec);
24652
+ this.srcSampleFmt = sourceSampleFmt(runtime.consts, cfg.sourceFormat);
24653
+ this.srcBytesPerSample = sourceBytesPerSample(cfg.sourceFormat);
24654
+ const { nav, consts } = runtime;
24655
+ const codecId = resolveAvCodecId(cfg.codec, consts);
24656
+ if (codecId === null) throw new Error(`audio-codec-nodeav: unknown codec '${cfg.codec}' for encode`);
24657
+ const codec = nav.Codec.findEncoder(codecId);
24658
+ if (!codec) throw new Error(`audio-codec-nodeav: encoder not registered for '${cfg.codec}'`);
24659
+ const fmts = codec.sampleFormats;
24660
+ this.encoderSampleFmt = fmts && fmts.length > 0 ? fmts[0] : consts.AV_SAMPLE_FMT_S16;
24661
+ this.outLayout = buildChannelLayout(consts, cfg.targetChannels);
24662
+ const ctx = new nav.CodecContext();
24663
+ ctx.allocContext3(codec);
24664
+ ctx.sampleRate = cfg.targetSampleRate;
24665
+ ctx.channelLayout = this.outLayout;
24666
+ ctx.sampleFormat = this.encoderSampleFmt;
24667
+ if (cfg.bitrateKbps !== void 0) ctx.bitRate = BigInt(Math.round(cfg.bitrateKbps * 1e3));
24668
+ const openRet = ctx.open2Sync(codec, null);
24669
+ if (openRet < 0) {
24670
+ ctx.freeContext();
24671
+ throw new Error(`audio-codec-nodeav: encoder open2 failed for '${cfg.codec}' (ret=${openRet})`);
24672
+ }
24673
+ this.frameSize = ctx.frameSize > 0 ? ctx.frameSize : 0;
24674
+ const swr = new nav.SoftwareResampleContext();
24675
+ const inLayout = buildChannelLayout(consts, cfg.sourceChannels);
24676
+ const allocRet = swr.allocSetOpts2(this.outLayout, this.encoderSampleFmt, cfg.targetSampleRate, inLayout, this.srcSampleFmt, cfg.sourceSampleRate);
24677
+ if (allocRet < 0) {
24678
+ ctx.freeContext();
24679
+ throw new Error(`audio-codec-nodeav: encode swr allocSetOpts2 failed (ret=${allocRet})`);
24680
+ }
24681
+ if (swr.init() < 0) {
24682
+ swr.free();
24683
+ ctx.freeContext();
24684
+ throw new Error("audio-codec-nodeav: encode swr init failed");
24685
+ }
24686
+ const fifo = new nav.AudioFifo();
24687
+ fifo.alloc(this.encoderSampleFmt, cfg.targetChannels, this.frameSize > 0 ? this.frameSize : 1024);
24688
+ const packet = new nav.Packet();
24689
+ packet.alloc();
24690
+ this.ctx = ctx;
24691
+ this.swr = swr;
24692
+ this.fifo = fifo;
24693
+ this.packet = packet;
24694
+ }
24695
+ /** Push one interleaved PCM chunk; emits encoded packets via `onChunk`. */
24696
+ pushPcm(data) {
24697
+ if (this.closed || data.byteLength === 0) return;
24698
+ const frameBytes = this.cfg.sourceChannels * this.srcBytesPerSample;
24699
+ const inSamples = Math.floor(data.byteLength / frameBytes);
24700
+ if (inSamples <= 0) return;
24701
+ this.resampleIntoFifo(data, inSamples);
24702
+ this.drainFifo(false);
24703
+ }
24704
+ /**
24705
+ * Flush the encoder: drain any partial frame left in the FIFO, then signal
24706
+ * end-of-stream so the codec emits its tail packets. Called before a graceful
24707
+ * close.
24708
+ */
24709
+ flush() {
24710
+ if (this.closed) return;
24711
+ this.drainFifo(true);
24712
+ try {
24713
+ if (this.ctx.sendFrameSync(null) >= 0) this.drainEncoder();
24714
+ } catch (err) {
24715
+ this.logger.warn("audio-codec-nodeav: encode flush failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
24716
+ }
24717
+ }
24718
+ resampleIntoFifo(data, inSamples) {
24719
+ const { nav } = this.runtime;
24720
+ const inFrame = new nav.Frame();
24721
+ inFrame.alloc();
24722
+ const outFrame = new nav.Frame();
24723
+ outFrame.alloc();
24724
+ try {
24725
+ inFrame.format = this.srcSampleFmt;
24726
+ inFrame.sampleRate = this.cfg.sourceSampleRate;
24727
+ inFrame.channelLayout = buildChannelLayout(this.runtime.consts, this.cfg.sourceChannels);
24728
+ inFrame.nbSamples = inSamples;
24729
+ const inBufRet = inFrame.getBuffer(0);
24730
+ if (inBufRet < 0) throw new Error(`inFrame.getBuffer failed (ret=${inBufRet})`);
24731
+ const planes = inFrame.extendedData;
24732
+ const plane0 = planes && planes.length > 0 ? planes[0] : null;
24733
+ if (!plane0) throw new Error("inFrame has no data plane");
24734
+ plane0.set(data.subarray(0, inSamples * this.cfg.sourceChannels * this.srcBytesPerSample));
24735
+ const outSamples = this.swr.getOutSamples(inSamples);
24736
+ if (outSamples <= 0) return;
24737
+ outFrame.format = this.encoderSampleFmt;
24738
+ outFrame.sampleRate = this.cfg.targetSampleRate;
24739
+ outFrame.channelLayout = this.outLayout;
24740
+ outFrame.nbSamples = outSamples;
24741
+ const outBufRet = outFrame.getBuffer(0);
24742
+ if (outBufRet < 0) throw new Error(`outFrame.getBuffer failed (ret=${outBufRet})`);
24743
+ const convRet = this.swr.convertFrame(outFrame, inFrame);
24744
+ if (convRet < 0) throw new Error(`swr.convertFrame failed (ret=${convRet})`);
24745
+ const produced = outFrame.nbSamples;
24746
+ if (produced <= 0) return;
24747
+ const outPlanes = outFrame.extendedData;
24748
+ if (!outPlanes || outPlanes.length === 0) return;
24749
+ this.fifo.writeSync(outPlanes, produced);
24750
+ } finally {
24751
+ inFrame.free();
24752
+ outFrame.free();
24753
+ }
24754
+ }
24755
+ /**
24756
+ * Pull `frameSize`-sized frames out of the FIFO and encode them. When
24757
+ * `flushTail` is set, also encode the final short frame (< frameSize) so no
24758
+ * tail samples are dropped on close.
24759
+ */
24760
+ drainFifo(flushTail) {
24761
+ const chunk = this.frameSize > 0 ? this.frameSize : this.fifo.size;
24762
+ if (chunk <= 0) return;
24763
+ while (this.fifo.size >= chunk && chunk > 0) {
24764
+ this.encodeFromFifo(chunk);
24765
+ if (this.frameSize <= 0) break;
24766
+ }
24767
+ if (flushTail && this.fifo.size > 0) this.encodeFromFifo(this.fifo.size);
24768
+ }
24769
+ encodeFromFifo(nbSamples) {
24770
+ if (nbSamples <= 0) return;
24771
+ const { nav } = this.runtime;
24772
+ const frame = new nav.Frame();
24773
+ frame.alloc();
24774
+ try {
24775
+ frame.format = this.encoderSampleFmt;
24776
+ frame.sampleRate = this.cfg.targetSampleRate;
24777
+ frame.channelLayout = this.outLayout;
24778
+ frame.nbSamples = nbSamples;
24779
+ const bufRet = frame.getBuffer(0);
24780
+ if (bufRet < 0) throw new Error(`encodeFrame.getBuffer failed (ret=${bufRet})`);
24781
+ const planes = frame.extendedData;
24782
+ if (!planes || planes.length === 0) throw new Error("encodeFrame has no data plane");
24783
+ const read = this.fifo.readSync(planes, nbSamples);
24784
+ if (read <= 0) return;
24785
+ frame.nbSamples = read;
24786
+ frame.pts = BigInt(this.ptsSamples);
24787
+ this.ptsSamples += read;
24788
+ const sendRet = this.ctx.sendFrameSync(frame);
24789
+ if (sendRet < 0) throw new Error(`sendFrame failed (ret=${sendRet})`);
24790
+ this.drainEncoder();
24791
+ } finally {
24792
+ frame.free();
24793
+ }
24794
+ }
24795
+ drainEncoder() {
24796
+ const { consts } = this.runtime;
24797
+ for (;;) {
24798
+ const recvRet = this.ctx.receivePacketSync(this.packet);
24799
+ if (recvRet === consts.AVERROR_EAGAIN || recvRet === consts.AVERROR_EOF) break;
24800
+ if (recvRet < 0) throw new Error(`audio-codec-nodeav: receivePacket failed (ret=${recvRet})`);
24801
+ try {
24802
+ const payload = this.packet.data;
24803
+ if (payload && payload.byteLength > 0) {
24804
+ const out = new Uint8Array(new ArrayBuffer(payload.byteLength));
24805
+ out.set(payload);
24806
+ const pktPts = this.packet.pts;
24807
+ const ptsMs = pktPts >= 0n ? Math.round(Number(pktPts) * 1e3 / this.cfg.targetSampleRate) : 0;
24808
+ this.onChunk({
24809
+ data: out,
24810
+ codec: this.codecName,
24811
+ pts: ptsMs,
24812
+ frameComplete: true
24813
+ });
24814
+ }
24815
+ } finally {
24816
+ this.packet.unref();
24817
+ }
24818
+ }
24819
+ }
24820
+ destroy() {
24821
+ if (this.closed) return;
24822
+ this.closed = true;
24823
+ this.freeQuietly(() => this.packet.free(), "packet");
24824
+ this.freeQuietly(() => this.fifo.free(), "fifo");
24825
+ this.freeQuietly(() => this.swr.free(), "swr");
24826
+ this.freeQuietly(() => this.ctx.freeContext(), "codecContext");
24827
+ }
24828
+ freeQuietly(fn, what) {
24829
+ try {
24830
+ fn();
24831
+ } catch (err) {
24832
+ this.logger.warn("audio-codec-nodeav: encode handle free failed", { meta: {
24833
+ what,
24834
+ error: err instanceof Error ? err.message : String(err)
24835
+ } });
24836
+ }
24837
+ }
24838
+ };
24839
+ //#endregion
24840
+ //#region src/audio-codec/provider.ts
24841
+ var DEFAULT_IDLE_MS = 3e4;
24842
+ var MAX_PCM_QUEUE_CHUNKS = 500;
24843
+ var REAPER_INTERVAL_MS = 5e3;
24844
+ /** Grace wait for the encoder to drain its tail after `flushEncode`. */
24845
+ var FLUSH_DRAIN_MS = 60;
24846
+ /**
24847
+ * Audio codec I/O box backed by **node-av's in-process libavcodec +
24848
+ * libswresample bindings** — the native counterpart to `audio-codec-ffmpeg`.
24849
+ * Decode/encode run in this addon's process (no subprocess), feeding raw
24850
+ * depacketized access units straight into a `CodecContext` (no ADTS/Ogg
24851
+ * container shim needed).
24852
+ *
24853
+ * This is a PLAIN provider (not a `BaseAddon`): it is owned and driven by the
24854
+ * `decoder-nodeav` addon, which loads the shared node-av runtime once and
24855
+ * hands it in. When node-av cannot be loaded on this node the owning addon
24856
+ * never constructs this provider, so the singleton slot falls back to
24857
+ * `audio-codec-ffmpeg`.
24858
+ */
24859
+ var NodeAvAudioCodecProvider = class {
24860
+ deps;
24861
+ sessions = /* @__PURE__ */ new Map();
24862
+ reaperTimer = null;
24863
+ constructor(deps) {
24864
+ this.deps = deps;
24865
+ }
24866
+ /** Arm the idle-session reaper. Called by the owning addon after construction. */
24867
+ start() {
24868
+ this.reaperTimer = setInterval(() => this.reapIdleSessions(), REAPER_INTERVAL_MS);
24869
+ if (typeof this.reaperTimer.unref === "function") this.reaperTimer.unref();
24870
+ }
24871
+ /** Stop the reaper and dispose every live session. Called on addon shutdown. */
24872
+ stop() {
24873
+ if (this.reaperTimer) {
24874
+ clearInterval(this.reaperTimer);
24875
+ this.reaperTimer = null;
24876
+ }
24877
+ for (const s of this.sessions.values()) this.disposeSession(s);
24878
+ this.sessions.clear();
24879
+ }
24880
+ async listSupportedCodecs() {
24881
+ return CODEC_CATALOG.map((e) => ({
24882
+ codec: e.codec,
24883
+ canDecode: e.canDecode,
24884
+ canEncode: e.canEncode,
24885
+ ...e.label ? { label: e.label } : {}
24886
+ }));
24887
+ }
24888
+ async canHandle(input) {
24889
+ return codecSupports(input.codec, input.kind);
24890
+ }
24891
+ async createDecodeSession(input) {
24892
+ const codec = resolveAudioCodecAlias(input.codec);
24893
+ const entry = catalogEntryFor(input.codec);
24894
+ if (!entry || !entry.canDecode) throw new Error(`audio-codec-nodeav: decode unsupported for codec '${input.codec}'`);
24895
+ const sessionId = `dec-${(0, node_crypto.randomUUID)()}`;
24896
+ const state = {
24897
+ sessionId,
24898
+ kind: "decode",
24899
+ config: {
24900
+ ...input,
24901
+ codec
24902
+ },
24903
+ ...input.tag ? { tag: input.tag } : {},
24904
+ createdAtMs: Date.now(),
24905
+ lastActivityMs: Date.now(),
24906
+ framesIn: 0,
24907
+ framesOut: 0,
24908
+ pcmQueue: [],
24909
+ session: null
24910
+ };
24911
+ state.session = this.spawnDecodeSession(state);
24912
+ this.sessions.set(sessionId, state);
24913
+ this.deps.logger.info("audio-codec-nodeav: decode session created", {
24914
+ tags: { sessionId },
24915
+ meta: {
24916
+ codec,
24917
+ target: `${input.targetSampleRate}Hz×${input.targetChannels}`
24918
+ }
24919
+ });
24920
+ return {
24921
+ sessionId,
24922
+ nodeId: this.deps.resolveLocalNodeId()
24923
+ };
24924
+ }
24925
+ async createEncodeSession(input) {
24926
+ const codec = resolveAudioCodecAlias(input.codec);
24927
+ const entry = catalogEntryFor(input.codec);
24928
+ if (!entry || !entry.canEncode) throw new Error(`audio-codec-nodeav: encode unsupported for codec '${input.codec}'`);
24929
+ const sessionId = `enc-${(0, node_crypto.randomUUID)()}`;
24930
+ const state = {
24931
+ sessionId,
24932
+ kind: "encode",
24933
+ config: {
24934
+ ...input,
24935
+ codec
24936
+ },
24937
+ ...input.tag ? { tag: input.tag } : {},
24938
+ createdAtMs: Date.now(),
24939
+ lastActivityMs: Date.now(),
24940
+ framesIn: 0,
24941
+ framesOut: 0,
24942
+ encodedQueue: [],
24943
+ session: null
24944
+ };
24945
+ state.session = this.spawnEncodeSession(state);
24946
+ this.sessions.set(sessionId, state);
24947
+ this.deps.logger.info("audio-codec-nodeav: encode session created", {
24948
+ tags: { sessionId },
24949
+ meta: {
24950
+ codec,
24951
+ target: `${input.targetSampleRate}Hz×${input.targetChannels}`
24952
+ }
24953
+ });
24954
+ return {
24955
+ sessionId,
24956
+ nodeId: this.deps.resolveLocalNodeId()
24957
+ };
24958
+ }
24959
+ async closeSession(input) {
24960
+ const s = this.sessions.get(input.sessionId);
24961
+ if (!s) return;
24962
+ this.disposeSession(s);
24963
+ this.sessions.delete(input.sessionId);
24964
+ }
24965
+ async pushEncodedFrame(input) {
24966
+ const s = this.sessions.get(input.sessionId);
24967
+ if (!s || s.kind !== "decode") throw new Error(`audio-codec-nodeav: decode session '${input.sessionId}' not found`);
24968
+ s.lastActivityMs = Date.now();
24969
+ s.framesIn++;
24970
+ if (!s.session) s.session = this.spawnDecodeSession(s);
24971
+ s.session.pushEncoded(input.data, input.pts);
24972
+ }
24973
+ async pullPcm(input) {
24974
+ const s = this.sessions.get(input.sessionId);
24975
+ if (!s || s.kind !== "decode") throw new Error(`audio-codec-nodeav: decode session '${input.sessionId}' not found`);
24976
+ s.lastActivityMs = Date.now();
24977
+ const out = s.pcmQueue.splice(0, input.maxCount);
24978
+ s.framesOut += out.length;
24979
+ return out;
24980
+ }
24981
+ async pushPcm(input) {
24982
+ const s = this.sessions.get(input.sessionId);
24983
+ if (!s || s.kind !== "encode") throw new Error(`audio-codec-nodeav: encode session '${input.sessionId}' not found`);
24984
+ s.lastActivityMs = Date.now();
24985
+ s.framesIn++;
24986
+ if (!s.session) s.session = this.spawnEncodeSession(s);
24987
+ s.session.pushPcm(input.data);
24988
+ }
24989
+ async pullEncoded(input) {
24990
+ const s = this.sessions.get(input.sessionId);
24991
+ if (!s || s.kind !== "encode") throw new Error(`audio-codec-nodeav: encode session '${input.sessionId}' not found`);
24992
+ s.lastActivityMs = Date.now();
24993
+ const out = s.encodedQueue.splice(0, input.maxCount);
24994
+ s.framesOut += out.length;
24995
+ return out;
24996
+ }
24997
+ async flushEncode(input) {
24998
+ const s = this.sessions.get(input.sessionId);
24999
+ if (!s || s.kind !== "encode") throw new Error(`audio-codec-nodeav: encode session '${input.sessionId}' not found`);
25000
+ s.lastActivityMs = Date.now();
25001
+ s.session?.flush();
25002
+ await new Promise((resolve) => setTimeout(resolve, FLUSH_DRAIN_MS));
25003
+ const out = s.encodedQueue.splice(0);
25004
+ s.framesOut += out.length;
25005
+ return out;
25006
+ }
25007
+ async listActiveSessions() {
25008
+ return [...this.sessions.values()].map((s) => ({
25009
+ sessionId: s.sessionId,
25010
+ kind: s.kind,
25011
+ codec: s.config.codec,
25012
+ sourceSampleRate: s.config.sourceSampleRate,
25013
+ sourceChannels: s.config.sourceChannels,
25014
+ targetSampleRate: s.config.targetSampleRate,
25015
+ targetChannels: s.config.targetChannels,
25016
+ format: this.resolveFormat(s),
25017
+ ...s.tag ? { tag: s.tag } : {},
25018
+ createdAtMs: s.createdAtMs,
25019
+ lastActivityMs: s.lastActivityMs,
25020
+ framesIn: s.framesIn,
25021
+ framesOut: s.framesOut
25022
+ }));
25023
+ }
25024
+ spawnDecodeSession(s) {
25025
+ return new NodeAvAudioDecodeSession(this.deps.runtime, {
25026
+ codec: s.config.codec,
25027
+ sourceSampleRate: s.config.sourceSampleRate,
25028
+ sourceChannels: s.config.sourceChannels,
25029
+ ...s.config.extraData ? { extraData: s.config.extraData } : {},
25030
+ targetSampleRate: s.config.targetSampleRate,
25031
+ targetChannels: s.config.targetChannels,
25032
+ targetFormat: this.pcmFormat(s.config.targetFormat)
25033
+ }, this.deps.logger, (chunk) => {
25034
+ s.pcmQueue.push(chunk);
25035
+ if (s.pcmQueue.length > MAX_PCM_QUEUE_CHUNKS) s.pcmQueue.splice(0, s.pcmQueue.length - MAX_PCM_QUEUE_CHUNKS);
25036
+ });
25037
+ }
25038
+ spawnEncodeSession(s) {
25039
+ return new NodeAvAudioEncodeSession(this.deps.runtime, {
25040
+ codec: s.config.codec,
25041
+ sourceSampleRate: s.config.sourceSampleRate,
25042
+ sourceChannels: s.config.sourceChannels,
25043
+ sourceFormat: this.pcmFormat(s.config.sourceFormat),
25044
+ targetSampleRate: s.config.targetSampleRate,
25045
+ targetChannels: s.config.targetChannels,
25046
+ ...s.config.bitrateKbps !== void 0 ? { bitrateKbps: s.config.bitrateKbps } : {}
25047
+ }, this.deps.logger, (chunk) => {
25048
+ s.encodedQueue.push(chunk);
25049
+ });
25050
+ }
25051
+ pcmFormat(format) {
25052
+ return format === "f32le" ? "f32le" : "s16le";
25053
+ }
25054
+ resolveFormat(s) {
25055
+ if (s.kind === "decode") return this.pcmFormat(s.config.targetFormat);
25056
+ return this.pcmFormat(s.config.sourceFormat);
25057
+ }
25058
+ reapIdleSessions() {
25059
+ const now = Date.now();
25060
+ for (const [id, s] of this.sessions) {
25061
+ const limit = s.config.idleMs ?? DEFAULT_IDLE_MS;
25062
+ if (now - s.lastActivityMs > limit) {
25063
+ this.deps.logger.info("audio-codec-nodeav: reaping idle session", {
25064
+ tags: { sessionId: id },
25065
+ meta: {
25066
+ kind: s.kind,
25067
+ idleMs: now - s.lastActivityMs,
25068
+ limit
25069
+ }
25070
+ });
25071
+ try {
25072
+ this.disposeSession(s);
25073
+ } catch (err) {
25074
+ this.deps.logger.warn("audio-codec-nodeav: dispose failed during reap", {
25075
+ tags: { sessionId: id },
25076
+ meta: { error: errMsg(err) }
25077
+ });
25078
+ }
25079
+ this.sessions.delete(id);
25080
+ }
25081
+ }
25082
+ }
25083
+ disposeSession(s) {
25084
+ try {
25085
+ s.session?.destroy();
25086
+ } catch (err) {
25087
+ this.deps.logger.warn("audio-codec-nodeav: session destroy failed", {
25088
+ tags: { sessionId: s.sessionId },
25089
+ meta: { error: errMsg(err) }
25090
+ });
25091
+ }
25092
+ s.session = null;
25093
+ }
25094
+ };
25095
+ //#endregion
25096
+ //#region src/audio-codec/nodeav-runtime-loader.ts
25097
+ var _runtime = null;
25098
+ /**
25099
+ * Load (or return the cached) node-av runtime. Rejects if the native binding
25100
+ * cannot be loaded on this node — the caller treats that as "node-av
25101
+ * unavailable" and declines to register the cap so the singleton falls back to
25102
+ * `audio-codec-ffmpeg`.
25103
+ */
25104
+ async function loadNodeAvRuntime() {
25105
+ if (_runtime) return _runtime;
25106
+ const [nav, consts] = await Promise.all([import("node-av"), import("node-av/constants")]);
25107
+ _runtime = {
25108
+ nav,
25109
+ consts
25110
+ };
25111
+ return _runtime;
25112
+ }
25113
+ /**
25114
+ * The already-loaded runtime, or `null` if `loadNodeAvRuntime` has not resolved
25115
+ * yet. Session runtimes read this synchronously — the addon guarantees it is
25116
+ * populated before any session is created.
25117
+ */
25118
+ function peekNodeAvRuntime() {
25119
+ return _runtime;
25120
+ }
25121
+ //#endregion
24303
25122
  //#region src/addon/index.ts
24304
25123
  var FRAME_BUFFER_CAPACITY = 32;
24305
25124
  var DecoderNodeAvAddon = class extends BaseAddon {
@@ -24325,6 +25144,12 @@ var DecoderNodeAvAddon = class extends BaseAddon {
24325
25144
  /** Running `getFrame` hit/miss counters surfaced via `getShmStats`. */
24326
25145
  getFrameHits = 0;
24327
25146
  getFrameMisses = 0;
25147
+ /**
25148
+ * node-av audio-codec provider — registered independently of the video
25149
+ * decoder backend gate whenever node-av loads on this node, so this single
25150
+ * addon provides both the `decoder` and `audio-codec` caps.
25151
+ */
25152
+ audioProvider = null;
24328
25153
  constructor() {
24329
25154
  super(DEFAULT_DECODER_HWACCEL_CONFIG);
24330
25155
  }
@@ -24359,20 +25184,40 @@ var DecoderNodeAvAddon = class extends BaseAddon {
24359
25184
  }] });
24360
25185
  }
24361
25186
  async onInitialize() {
25187
+ const registrations = [];
25188
+ let runtime = null;
25189
+ try {
25190
+ runtime = await loadNodeAvRuntime();
25191
+ } catch (err) {
25192
+ this.ctx.logger.error("decoder-nodeav: node-av failed to load — no audio-codec provider", { meta: { error: err instanceof Error ? err.message : String(err) } });
25193
+ }
25194
+ if (runtime) {
25195
+ this.audioProvider = new NodeAvAudioCodecProvider({
25196
+ logger: this.ctx.logger,
25197
+ runtime,
25198
+ resolveLocalNodeId: () => this.resolveLocalNodeId()
25199
+ });
25200
+ this.audioProvider.start();
25201
+ registrations.push({
25202
+ capability: audioCodecCapability,
25203
+ provider: this.audioProvider
25204
+ });
25205
+ }
24362
25206
  const backend = await resolveDecoderBackend(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
24363
25207
  if (backend !== "nodeav") {
24364
25208
  this.ctx.logger.info("node-av decoder: this node selects a different decoder backend — standing down (no decoder provider registered)", { meta: { selectedBackend: backend } });
24365
- return [];
25209
+ return registrations;
24366
25210
  }
24367
25211
  this.ctx.logger.info("node-av decoder addon initialized", { meta: { selectedBackend: backend } });
24368
25212
  this.frameReaders = new _camstack_shm_ring.FrameRingReaderCache(this.ctx.logger);
24369
25213
  if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
24370
25214
  this.ctx.logger.warn("nodeav: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
24371
25215
  });
24372
- return [{
25216
+ registrations.push({
24373
25217
  capability: decoderCapability,
24374
25218
  provider: this
24375
- }];
25219
+ });
25220
+ return registrations;
24376
25221
  }
24377
25222
  /**
24378
25223
  * Resolve the effective hwaccel backend for a new session — from THIS addon's
@@ -24618,6 +25463,8 @@ var DecoderNodeAvAddon = class extends BaseAddon {
24618
25463
  };
24619
25464
  }
24620
25465
  async onShutdown() {
25466
+ this.audioProvider?.stop();
25467
+ this.audioProvider = null;
24621
25468
  this.ctx.logger.info("node-av decoder addon shutdown — destroying all sessions");
24622
25469
  const destroyPromises = [];
24623
25470
  for (const [sessionId, session] of this.sessions) {
@@ -24638,6 +25485,11 @@ var DecoderNodeAvAddon = class extends BaseAddon {
24638
25485
  //#endregion
24639
25486
  exports.DecoderFrameRingSink = DecoderFrameRingSink;
24640
25487
  exports.DecoderNodeAvAddon = DecoderNodeAvAddon;
25488
+ exports.NodeAvAudioCodecProvider = NodeAvAudioCodecProvider;
25489
+ exports.NodeAvAudioDecodeSession = NodeAvAudioDecodeSession;
25490
+ exports.NodeAvAudioEncodeSession = NodeAvAudioEncodeSession;
24641
25491
  exports.NodeAvDecoderSession = NodeAvDecoderSession;
24642
25492
  exports.default = DecoderNodeAvAddon;
25493
+ exports.loadNodeAvRuntime = loadNodeAvRuntime;
24643
25494
  exports.makeSegmentName = makeSegmentName;
25495
+ exports.peekNodeAvRuntime = peekNodeAvRuntime;