@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.
- package/dist/core/contracts.d.ts +28 -14
- package/dist/index.js +70 -11
- package/dist/index.js.map +2 -2
- package/dist/transport/p2p/command-router.d.ts +5 -0
- package/dist/transport/p2p/talkback.d.ts +21 -10
- package/package.json +1 -1
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
|
@@ -19923,7 +19923,8 @@ var AdtsFrameReader = class {
|
|
|
19923
19923
|
};
|
|
19924
19924
|
|
|
19925
19925
|
// dist/transport/p2p/talkback.js
|
|
19926
|
-
var
|
|
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
|
-
|
|
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 +
|
|
19994
|
+
this.nextDueAt = now2 + this.frameMs;
|
|
19981
19995
|
this.lastWriteAt = now2;
|
|
19982
|
-
this.timer = setInterval(() => this.tick(),
|
|
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.
|
|
19989
|
-
*
|
|
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 +
|
|
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 +=
|
|
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
|
-
|
|
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);
|