@mega-yfue/eufy-sdk 0.5.0-beta.18 → 0.5.0-beta.19

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.
@@ -548,6 +548,8 @@ export interface LiveVideoConfig {
548
548
  * 7 → `mp4a.40.39`); it fails the stream on anything else.
549
549
  */
550
550
  export type AudioCodec = "aac-lc" | "aac-eld" | "g711a";
551
+ /** The codec a camera's speaker plays: `aac-eld` on a camera whose speaker plays it, `aac-lc` otherwise. */
552
+ export type TalkbackCodec = Extract<AudioCodec, "aac-lc" | "aac-eld">;
551
553
  /**
552
554
  * One audio access unit, carrying the codec the station declared for it.
553
555
  *
@@ -877,10 +879,11 @@ export interface MediaProvider {
877
879
  }): Promise<Record<string, unknown>>;
878
880
  }
879
881
  /**
880
- * An encoder that turns raw PCM into AAC-LC frames, supplied by the CALLER. The SDK ships none: the
881
- * device's audio path is fixed at AAC-LC 16 kHz mono, and every plausible encoder is either a native
882
+ * An encoder that turns raw PCM into AAC-LC frames, supplied by the CALLER. The SDK ships none: an
883
+ * `aac-lc` speaker plays AAC-LC 16 kHz mono, and every plausible encoder is either a native
882
884
  * dependency or an external process, both of which belong to the host rather than to a protocol SDK.
883
- * It is unnecessary where the audio is already AAC — see {@link TalkbackHandle}.
885
+ * It is unnecessary where the audio is already AAC — see {@link TalkbackHandle}. A camera whose speaker
886
+ * plays `aac-eld` refuses an encoder: a talkback opened with one is rejected before the speaker path opens.
884
887
  *
885
888
  * `encode` receives 16-bit little-endian mono PCM at 16 kHz and returns whole ADTS frames, zero or
886
889
  * more per call (an encoder buffers until it has a full 1024-sample block). `flush` drains a partial
@@ -895,17 +898,19 @@ export interface AacEncoder {
895
898
  * A live talkback session: audio pushed from the host to a camera's speaker, the mirror of
896
899
  * {@link LiveStreamHandle}'s inbound feed.
897
900
  *
898
- * Audio must be **AAC-LC, 16 kHz, mono, in ADTS frames** — what the device's path is fixed at, so a
899
- * stream at another rate or channel count is rejected rather than resampled (it would otherwise play
900
- * at the wrong pitch and speed). Feed it either way:
901
+ * Audio must be in the codec the camera's speaker plays, {@link TalkbackHandle.codec}:
901
902
  *
902
- * - **ADTS AAC** — the default. Chunk boundaries are irrelevant; frames are recovered from the
903
- * stream, so piping an encoder's output straight in works.
904
- * - **PCM** — only when the handle was opened with an {@link AacEncoder}, which then does the
905
- * conversion. `write` takes 16-bit little-endian mono PCM at 16 kHz instead.
903
+ * - **`aac-lc`:** AAC-LC, 16 kHz, mono, in ADTS frames, through `write` / `writable()`. Chunk boundaries are
904
+ * irrelevant; frames are recovered from the stream. With an {@link AacEncoder}, `write` takes 16-bit
905
+ * little-endian mono PCM at 16 kHz instead.
906
+ * - **`aac-eld`:** one raw AAC-ELD access unit with LD-SBR (16 kHz, mono, 512 samples) per `write`, or per
907
+ * object-mode `writable()` write. No encoder applies.
906
908
  *
907
- * Frames are **paced** at their own playback rate (64 ms each) rather than flushed as fast as they
908
- * arrive, so feeding a file plays it at speed instead of overrunning the device. A live source keeps
909
+ * An `aac-lc` stream at another rate or channel count is rejected rather than resampled. An `aac-eld` unit longer
910
+ * than the device accepts is refused with an `error`.
911
+ *
912
+ * Frames are **paced** at their own playback rate (64 ms for `aac-lc`, 32 ms for `aac-eld`) rather than flushed as
913
+ * fast as they arrive, so feeding a file plays it at speed instead of overrunning the device. A live source keeps
909
914
  * the queue near-empty and is unaffected.
910
915
  *
911
916
  * @example
@@ -917,7 +922,16 @@ export interface AacEncoder {
917
922
  * ```
918
923
  */
919
924
  export interface TalkbackHandle {
920
- /** Queue audio — ADTS frames, or PCM when an encoder was supplied. Partial frames are held. */
925
+ /**
926
+ * The codec this camera's speaker plays: `aac-eld` on a camera whose speaker plays it and whose stream on the same
927
+ * media session carries `aac-eld`, `aac-lc` otherwise, including when that stream carries no audio. It decides
928
+ * what {@link write} and {@link writable} take.
929
+ */
930
+ readonly codec: TalkbackCodec;
931
+ /**
932
+ * Queue audio: ADTS frames or PCM for `aac-lc`, one access unit per call for `aac-eld`. Partial ADTS frames are
933
+ * held.
934
+ */
921
935
  write(chunk: Buffer): void;
922
936
  /**
923
937
  * A `node:stream` Writable over {@link write}, for piping a file or an encoder's stdout. Applies
@@ -940,7 +954,7 @@ export interface TalkbackHandle {
940
954
  *
941
955
  * This deliberately does NOT fire on a merely-empty queue. A realtime source keeps the queue near
942
956
  * empty by design, so "queue is empty" arrives after the very first frame and stopping on it would
943
- * cut the clip to 64 ms. {@link pending} is the instantaneous depth.
957
+ * cut the clip to one frame. {@link pending} is the instantaneous depth.
944
958
  */
945
959
  on(event: "finished", listener: () => void): this;
946
960
  /**
package/dist/index.js CHANGED
@@ -19923,7 +19923,8 @@ var AdtsFrameReader = class {
19923
19923
  };
19924
19924
 
19925
19925
  // dist/transport/p2p/talkback.js
19926
- var DEFAULT_HIGH_WATER_FRAMES = Math.ceil(2e3 / AAC_FRAME_MS);
19926
+ var HIGH_WATER_MS = 2e3;
19927
+ var TALKBACK_ELD_FRAME_MS = 32;
19927
19928
  var DEFAULT_IDLE_TIMEOUT_MS = 3e4;
19928
19929
  var Talkback = class extends EventEmitter5 {
19929
19930
  sink;
@@ -19931,7 +19932,8 @@ var Talkback = class extends EventEmitter5 {
19931
19932
  reader = new AdtsFrameReader();
19932
19933
  queue = [];
19933
19934
  logger;
19934
- highWater;
19935
+ /** Playback duration of one queued frame, the pacer's step: 64 ms for `aac-lc`, 32 ms for `aac-eld`. */
19936
+ frameMs = AAC_FRAME_MS;
19935
19937
  idleTimeoutMs;
19936
19938
  timer;
19937
19939
  started = false;
@@ -19957,9 +19959,14 @@ var Talkback = class extends EventEmitter5 {
19957
19959
  this.sink = sink;
19958
19960
  this.opts = opts;
19959
19961
  this.logger = opts.logger ?? noopLogger;
19960
- this.highWater = opts.highWaterFrames ?? DEFAULT_HIGH_WATER_FRAMES;
19961
19962
  this.idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
19962
19963
  }
19964
+ /** Frames the pacing queue holds before a writable applies backpressure: {@link HIGH_WATER_MS} of this codec. */
19965
+ get highWater() {
19966
+ return this.opts.highWaterFrames ?? Math.ceil(HIGH_WATER_MS / this.frameMs);
19967
+ }
19968
+ /** The codec this talkback sends, set by {@link start}. */
19969
+ codec = "aac-lc";
19963
19970
  /** Frames still queued for the wire. */
19964
19971
  get pending() {
19965
19972
  return this.queue.length;
@@ -19968,25 +19975,33 @@ var Talkback = class extends EventEmitter5 {
19968
19975
  * Open the device's talkback path and begin the pacing tick. Throws when the topology's control
19969
19976
  * frame could not be sent — for a HomeBase-attached camera that means the level-2 key was never
19970
19977
  * negotiated, which would otherwise leave a silent session that accepts audio nobody hears.
19978
+ *
19979
+ * `codec` is the codec the camera's speaker plays; it fixes the input this talkback accepts and the pacing step.
19971
19980
  */
19972
- start() {
19981
+ start(codec) {
19973
19982
  if (this.started)
19974
19983
  return this;
19984
+ if (codec === "aac-eld" && this.opts.encoder) {
19985
+ throw new Error("talkback: this camera plays aac-eld; an AacEncoder produces aac-lc");
19986
+ }
19987
+ this.codec = codec;
19988
+ this.frameMs = codec === "aac-eld" ? TALKBACK_ELD_FRAME_MS : AAC_FRAME_MS;
19975
19989
  if (!this.sink.startTalkback(this.opts.channel, this.opts.homeBaseAttached)) {
19976
19990
  throw new Error(`talkback: could not open the audio path on channel ${this.opts.channel}`);
19977
19991
  }
19978
19992
  this.started = true;
19979
19993
  const now2 = Date.now();
19980
- this.nextDueAt = now2 + AAC_FRAME_MS;
19994
+ this.nextDueAt = now2 + this.frameMs;
19981
19995
  this.lastWriteAt = now2;
19982
- this.timer = setInterval(() => this.tick(), AAC_FRAME_MS);
19996
+ this.timer = setInterval(() => this.tick(), this.frameMs);
19983
19997
  this.timer.unref?.();
19984
19998
  this.sink.on?.("audioGap", this.onAudioGap);
19985
19999
  return this;
19986
20000
  }
19987
20001
  /**
19988
- * Queue audio. Bytes are ADTS AAC, or PCM when an encoder was supplied; either way the input is
19989
- * treated as a stream, so a chunk carrying part of a frame is held until the rest arrives.
20002
+ * Queue audio in the codec this talkback sends. For `aac-lc` the bytes are ADTS, or PCM when an encoder was
20003
+ * supplied, read as a stream, so a chunk carrying part of a frame is held until the rest arrives. For `aac-eld`
20004
+ * each call is one whole access unit; a unit over the device's length limit is dropped with an `error`.
19990
20005
  *
19991
20006
  * Audio written after {@link end} is dropped with an `error` rather than queued: `finished` has
19992
20007
  * either already fired or is owed on the frames written before it, so a late chunk would either play
@@ -20000,6 +20015,15 @@ var Talkback = class extends EventEmitter5 {
20000
20015
  return;
20001
20016
  }
20002
20017
  this.lastWriteAt = Date.now();
20018
+ if (this.codec === "aac-eld") {
20019
+ if (chunk.length > MAX_AUDIO_FRAME_BYTES) {
20020
+ this.fail(new Error(`talkback: frame of ${chunk.length} B exceeds the device's ${MAX_AUDIO_FRAME_BYTES} B limit`));
20021
+ return;
20022
+ }
20023
+ if (chunk.length)
20024
+ this.queue.push(chunk);
20025
+ return;
20026
+ }
20003
20027
  if (!this.opts.encoder) {
20004
20028
  this.enqueue(this.reader.push(chunk));
20005
20029
  return;
@@ -20068,14 +20092,14 @@ var Talkback = class extends EventEmitter5 {
20068
20092
  return;
20069
20093
  const now2 = Date.now();
20070
20094
  if (!this.queue.length) {
20071
- this.nextDueAt = now2 + AAC_FRAME_MS;
20095
+ this.nextDueAt = now2 + this.frameMs;
20072
20096
  this.idleCheck(now2);
20073
20097
  this.announceFinishIfDone();
20074
20098
  return;
20075
20099
  }
20076
20100
  while (this.queue.length && now2 >= this.nextDueAt) {
20077
20101
  const frame = this.queue.shift();
20078
- this.nextDueAt += AAC_FRAME_MS;
20102
+ this.nextDueAt += this.frameMs;
20079
20103
  try {
20080
20104
  this.sink.sendAudioFrame(this.opts.channel, frame);
20081
20105
  } catch (e) {
@@ -20133,9 +20157,11 @@ var Talkback = class extends EventEmitter5 {
20133
20157
  /**
20134
20158
  * A Writable over {@link write} that withholds its callback while the queue is at the high-water
20135
20159
  * mark — which is what turns `pipe()` from a file into playback at speed rather than a memory spike.
20160
+ * For `aac-eld` it is object-mode: each write is one access unit.
20136
20161
  */
20137
20162
  writable() {
20138
20163
  return new Writable({
20164
+ objectMode: this.codec === "aac-eld",
20139
20165
  write: (chunk, _enc, cb) => {
20140
20166
  this.write(chunk);
20141
20167
  if (this.queue.length < this.highWater)
@@ -20863,6 +20889,30 @@ var P2P_STATION_WAITS = {
20863
20889
  level2Settle: LEVEL2_SETTLE_MS
20864
20890
  };
20865
20891
  var RTSP_URL_READ_TIMEOUT_MS = 12e3;
20892
+ var TALKBACK_CODEC_WAIT_MS = 3e3;
20893
+ var ELD_TALKBACK_MODELS = /* @__PURE__ */ new Set(["T8410C"]);
20894
+ function speakerCodec(consumer, talk, timeoutMs) {
20895
+ return new Promise((resolve) => {
20896
+ let timer;
20897
+ const settle = (codec) => {
20898
+ clearTimeout(timer);
20899
+ consumer.off("video", onVideo);
20900
+ consumer.off("audio", onAudio);
20901
+ consumer.off("stop", onStop);
20902
+ talk.off("stop", onStop);
20903
+ resolve(codec);
20904
+ };
20905
+ const onVideo = () => {
20906
+ timer ??= setTimeout(() => settle("aac-lc"), timeoutMs);
20907
+ };
20908
+ const onAudio = (frame) => settle(frame.codec === "aac-eld" ? "aac-eld" : "aac-lc");
20909
+ const onStop = () => settle("aac-lc");
20910
+ consumer.on("video", onVideo);
20911
+ consumer.on("audio", onAudio);
20912
+ consumer.on("stop", onStop);
20913
+ talk.once("stop", onStop);
20914
+ });
20915
+ }
20866
20916
  var SHARED_LIVE_OPT_KEYS = [
20867
20917
  "eccPrivateKey",
20868
20918
  "keepAliveMs",
@@ -21416,6 +21466,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
21416
21466
  * stream costs nothing extra and a talkback on an otherwise idle camera opens the session it needs
21417
21467
  * instead of playing into silence.
21418
21468
  *
21469
+ * **On a model in {@link ELD_TALKBACK_MODELS}, the speaker plays the codec the camera sends.** So the first
21470
+ * audio frame of the media session this talkback holds decides its codec ({@link speakerCodec}), within
21471
+ * {@link TALKBACK_CODEC_WAIT_MS} of the session's first frame, and the claim is re-checked after that wait as
21472
+ * after the warm-up. Every other model opens `aac-lc` at once.
21473
+ *
21419
21474
  * The level-2 key is waited for softly: only the HomeBase-attached path requires it, and
21420
21475
  * {@link Talkback.start} reports that failure precisely, so a hard wait here would reject an
21421
21476
  * own-session camera that legitimately never negotiates one.
@@ -21485,7 +21540,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
21485
21540
  consumer.on("stop", () => {
21486
21541
  void talk.stop().catch((e) => logger.warn?.(`talkback: stop for ${sn} failed: ${String(e)}`));
21487
21542
  });
21488
- return talk.start();
21543
+ const codec = ELD_TALKBACK_MODELS.has(this.recordFor(sn)?.model ?? "") ? await speakerCodec(consumer, talk, TALKBACK_CODEC_WAIT_MS) : "aac-lc";
21544
+ if (this.talkbacks.get(key) !== talk) {
21545
+ throw new Error(`talkback: ${sn} was closed while its speaker codec was being read`);
21546
+ }
21547
+ return talk.start(codec);
21489
21548
  } catch (e) {
21490
21549
  if (this.talkbacks.get(key) === talk)
21491
21550
  this.talkbacks.delete(key);