@mega-yfue/eufy-sdk 0.5.0-beta.17 → 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
@@ -6233,6 +6233,9 @@ var LIGHT_MEMBERS = {
6233
6233
  * This is the switch a user changes and expects to STAY changed. {@link isOn} is a different fact:
6234
6234
  * the lamp being lit right now, driven by whichever
6235
6235
  * client is streaming — the vendor app lights it for a live view and drops it on quitting.
6236
+ *
6237
+ * The T8423 write takes the session-chosen level; level-1 int+string was verified live on a standalone
6238
+ * unit.
6236
6239
  */
6237
6240
  spotlightEnabled: {
6238
6241
  param: LIGHT_CMD.SPOTLIGHT_ENABLE,
@@ -6240,7 +6243,7 @@ var LIGHT_MEMBERS = {
6240
6243
  kind: "boolean",
6241
6244
  provenance: "verified",
6242
6245
  description: "The spotlight master switch, distinct from the on/off above \u2014 whether the spotlight may light at all. Read verified live on a T8170 (param 1403, direct polarity: 1 = enabled).",
6243
- write: (v, ctx) => directBinary(LIGHT_CMD.SPOTLIGHT_ENABLE, asBool(v) ? 1 : 0, ctx),
6246
+ write: (v, ctx) => setScalar(LIGHT_CMD.SPOTLIGHT_ENABLE, asBool(v) ? 1 : 0, ctx, ctx.deviceType === DeviceType.FLOODLIGHT_CAMERA_8423 ? "auto" : "direct-binary"),
6244
6247
  writeAs: "setEnabled"
6245
6248
  }
6246
6249
  };
@@ -19920,7 +19923,8 @@ var AdtsFrameReader = class {
19920
19923
  };
19921
19924
 
19922
19925
  // dist/transport/p2p/talkback.js
19923
- var DEFAULT_HIGH_WATER_FRAMES = Math.ceil(2e3 / AAC_FRAME_MS);
19926
+ var HIGH_WATER_MS = 2e3;
19927
+ var TALKBACK_ELD_FRAME_MS = 32;
19924
19928
  var DEFAULT_IDLE_TIMEOUT_MS = 3e4;
19925
19929
  var Talkback = class extends EventEmitter5 {
19926
19930
  sink;
@@ -19928,7 +19932,8 @@ var Talkback = class extends EventEmitter5 {
19928
19932
  reader = new AdtsFrameReader();
19929
19933
  queue = [];
19930
19934
  logger;
19931
- 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;
19932
19937
  idleTimeoutMs;
19933
19938
  timer;
19934
19939
  started = false;
@@ -19954,9 +19959,14 @@ var Talkback = class extends EventEmitter5 {
19954
19959
  this.sink = sink;
19955
19960
  this.opts = opts;
19956
19961
  this.logger = opts.logger ?? noopLogger;
19957
- this.highWater = opts.highWaterFrames ?? DEFAULT_HIGH_WATER_FRAMES;
19958
19962
  this.idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
19959
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";
19960
19970
  /** Frames still queued for the wire. */
19961
19971
  get pending() {
19962
19972
  return this.queue.length;
@@ -19965,25 +19975,33 @@ var Talkback = class extends EventEmitter5 {
19965
19975
  * Open the device's talkback path and begin the pacing tick. Throws when the topology's control
19966
19976
  * frame could not be sent — for a HomeBase-attached camera that means the level-2 key was never
19967
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.
19968
19980
  */
19969
- start() {
19981
+ start(codec) {
19970
19982
  if (this.started)
19971
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;
19972
19989
  if (!this.sink.startTalkback(this.opts.channel, this.opts.homeBaseAttached)) {
19973
19990
  throw new Error(`talkback: could not open the audio path on channel ${this.opts.channel}`);
19974
19991
  }
19975
19992
  this.started = true;
19976
19993
  const now2 = Date.now();
19977
- this.nextDueAt = now2 + AAC_FRAME_MS;
19994
+ this.nextDueAt = now2 + this.frameMs;
19978
19995
  this.lastWriteAt = now2;
19979
- this.timer = setInterval(() => this.tick(), AAC_FRAME_MS);
19996
+ this.timer = setInterval(() => this.tick(), this.frameMs);
19980
19997
  this.timer.unref?.();
19981
19998
  this.sink.on?.("audioGap", this.onAudioGap);
19982
19999
  return this;
19983
20000
  }
19984
20001
  /**
19985
- * Queue audio. Bytes are ADTS AAC, or PCM when an encoder was supplied; either way the input is
19986
- * 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`.
19987
20005
  *
19988
20006
  * Audio written after {@link end} is dropped with an `error` rather than queued: `finished` has
19989
20007
  * either already fired or is owed on the frames written before it, so a late chunk would either play
@@ -19997,6 +20015,15 @@ var Talkback = class extends EventEmitter5 {
19997
20015
  return;
19998
20016
  }
19999
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
+ }
20000
20027
  if (!this.opts.encoder) {
20001
20028
  this.enqueue(this.reader.push(chunk));
20002
20029
  return;
@@ -20065,14 +20092,14 @@ var Talkback = class extends EventEmitter5 {
20065
20092
  return;
20066
20093
  const now2 = Date.now();
20067
20094
  if (!this.queue.length) {
20068
- this.nextDueAt = now2 + AAC_FRAME_MS;
20095
+ this.nextDueAt = now2 + this.frameMs;
20069
20096
  this.idleCheck(now2);
20070
20097
  this.announceFinishIfDone();
20071
20098
  return;
20072
20099
  }
20073
20100
  while (this.queue.length && now2 >= this.nextDueAt) {
20074
20101
  const frame = this.queue.shift();
20075
- this.nextDueAt += AAC_FRAME_MS;
20102
+ this.nextDueAt += this.frameMs;
20076
20103
  try {
20077
20104
  this.sink.sendAudioFrame(this.opts.channel, frame);
20078
20105
  } catch (e) {
@@ -20130,9 +20157,11 @@ var Talkback = class extends EventEmitter5 {
20130
20157
  /**
20131
20158
  * A Writable over {@link write} that withholds its callback while the queue is at the high-water
20132
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.
20133
20161
  */
20134
20162
  writable() {
20135
20163
  return new Writable({
20164
+ objectMode: this.codec === "aac-eld",
20136
20165
  write: (chunk, _enc, cb) => {
20137
20166
  this.write(chunk);
20138
20167
  if (this.queue.length < this.highWater)
@@ -20860,6 +20889,30 @@ var P2P_STATION_WAITS = {
20860
20889
  level2Settle: LEVEL2_SETTLE_MS
20861
20890
  };
20862
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
+ }
20863
20916
  var SHARED_LIVE_OPT_KEYS = [
20864
20917
  "eccPrivateKey",
20865
20918
  "keepAliveMs",
@@ -21413,6 +21466,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
21413
21466
  * stream costs nothing extra and a talkback on an otherwise idle camera opens the session it needs
21414
21467
  * instead of playing into silence.
21415
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
+ *
21416
21474
  * The level-2 key is waited for softly: only the HomeBase-attached path requires it, and
21417
21475
  * {@link Talkback.start} reports that failure precisely, so a hard wait here would reject an
21418
21476
  * own-session camera that legitimately never negotiates one.
@@ -21482,7 +21540,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
21482
21540
  consumer.on("stop", () => {
21483
21541
  void talk.stop().catch((e) => logger.warn?.(`talkback: stop for ${sn} failed: ${String(e)}`));
21484
21542
  });
21485
- 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);
21486
21548
  } catch (e) {
21487
21549
  if (this.talkbacks.get(key) === talk)
21488
21550
  this.talkbacks.delete(key);