@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.
package/dist/core/contracts.d.ts
CHANGED
|
@@ -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:
|
|
881
|
-
*
|
|
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
|
|
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
|
-
* -
|
|
903
|
-
* stream
|
|
904
|
-
*
|
|
905
|
-
*
|
|
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
|
-
*
|
|
908
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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 +
|
|
19994
|
+
this.nextDueAt = now2 + this.frameMs;
|
|
19978
19995
|
this.lastWriteAt = now2;
|
|
19979
|
-
this.timer = setInterval(() => this.tick(),
|
|
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.
|
|
19986
|
-
*
|
|
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 +
|
|
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 +=
|
|
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
|
-
|
|
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);
|