@mega-yfue/eufy-sdk 0.5.0-beta.6 → 0.5.0-beta.8
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/client/eufy-mega.d.ts +8 -5
- package/dist/index.js +62 -15
- package/dist/index.js.map +2 -2
- package/dist/transport/p2p/command-router.d.ts +8 -5
- package/dist/transport/p2p/live-stream.d.ts +3 -0
- package/dist/transport/p2p/p2p-session.d.ts +5 -0
- package/dist/transport/p2p/video.d.ts +3 -2
- package/package.json +1 -1
|
@@ -835,11 +835,14 @@ export declare class EufyMega extends EventEmitter {
|
|
|
835
835
|
*/
|
|
836
836
|
setProperty(sn: string, name: string, value: boolean | number | string): Promise<void>;
|
|
837
837
|
/**
|
|
838
|
-
* Restart a HomeBase.
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
*
|
|
838
|
+
* Restart a station: a HomeBase, or a camera-family device that is its own station.
|
|
839
|
+
*
|
|
840
|
+
* The restart is `RESTART_HUB` on the station's broadcast channel, so it reaches whatever owns the
|
|
841
|
+
* P2P session. A standalone camera or doorbell owns its own session and is the station it restarts.
|
|
842
|
+
* A camera attached to a HomeBase is not — the same frame would restart its HomeBase — so its serial
|
|
843
|
+
* throws, as does any non-camera serial, rather than doing nothing. The device drops its connection
|
|
844
|
+
* and returns after a minute or two; a HomeBase takes everything behind it offline meanwhile.
|
|
845
|
+
* Verified on real hardware against a HomeBase and a standalone indoor camera.
|
|
843
846
|
*/
|
|
844
847
|
reboot(sn: string): Promise<void>;
|
|
845
848
|
/**
|
package/dist/index.js
CHANGED
|
@@ -16249,6 +16249,21 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16249
16249
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16250
16250
|
return true;
|
|
16251
16251
|
}
|
|
16252
|
+
/**
|
|
16253
|
+
* The level-1 twin of {@link sendRawLevel2Bytes}: the plaintext `payload` sealed AES-128-ECB under the
|
|
16254
|
+
* level-1 key, signCode 1 — the seal of the level-1 media start. Returns `false` when not connected.
|
|
16255
|
+
*/
|
|
16256
|
+
sendRawLevel1Bytes(payload, channel, outerCmd) {
|
|
16257
|
+
if (!this.connectAddress)
|
|
16258
|
+
return false;
|
|
16259
|
+
const data = Buffer.concat([
|
|
16260
|
+
buildCommandHeader(this.seqNumber, outerCmd),
|
|
16261
|
+
buildRawCommandPayload(encryptP2PData(paddingP2PData(payload), this.level1Key), channel, 1)
|
|
16262
|
+
]);
|
|
16263
|
+
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16264
|
+
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16265
|
+
return true;
|
|
16266
|
+
}
|
|
16252
16267
|
/**
|
|
16253
16268
|
* Stop the realtime media stream (`CMD_STOP_REALTIME_MEDIA`, 1004) on a camera `channel`, in the shape
|
|
16254
16269
|
* its runtime topology takes, as {@link startLiveMedia} selects its start:
|
|
@@ -16810,11 +16825,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16810
16825
|
if (advance > SEQUENCE_LOOKBACK) {
|
|
16811
16826
|
if (65536 - advance <= STALE_RETRANSMIT_DEPTH)
|
|
16812
16827
|
return;
|
|
16813
|
-
if (this.pendingByDataType.has(dataType) && this.tracedDatagramGaps++ < MAX_TRACED_DATAGRAM_GAPS)
|
|
16828
|
+
if ((dataType === P2PDataType.VIDEO || this.pendingByDataType.has(dataType)) && this.tracedDatagramGaps++ < MAX_TRACED_DATAGRAM_GAPS)
|
|
16814
16829
|
this.trace({ phase: "sequence-restart", dataType });
|
|
16815
16830
|
this.clearReorderTimer(dataType);
|
|
16816
16831
|
this.reorderByType.delete(dataType);
|
|
16817
16832
|
this.pendingByDataType.delete(dataType);
|
|
16833
|
+
if (dataType === P2PDataType.VIDEO)
|
|
16834
|
+
this.emit("videoGap");
|
|
16818
16835
|
this.lastSeqByType.set(dataType, seqNo);
|
|
16819
16836
|
this.reassemble(dataType, msg.subarray(8));
|
|
16820
16837
|
return;
|
|
@@ -16880,9 +16897,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16880
16897
|
*/
|
|
16881
16898
|
abandonHole(dataType) {
|
|
16882
16899
|
const held = this.reorderByType.get(dataType).held;
|
|
16883
|
-
if (this.pendingByDataType.has(dataType) && this.tracedDatagramGaps++ < MAX_TRACED_DATAGRAM_GAPS)
|
|
16900
|
+
if ((dataType === P2PDataType.VIDEO || this.pendingByDataType.has(dataType)) && this.tracedDatagramGaps++ < MAX_TRACED_DATAGRAM_GAPS)
|
|
16884
16901
|
this.trace({ phase: "datagram-gap", dataType });
|
|
16885
16902
|
this.pendingByDataType.delete(dataType);
|
|
16903
|
+
if (dataType === P2PDataType.VIDEO)
|
|
16904
|
+
this.emit("videoGap");
|
|
16886
16905
|
const last = this.lastSeqByType.get(dataType);
|
|
16887
16906
|
const earliest = [...held.keys()].sort((a, b) => (a - last & 65535) - (b - last & 65535))[0];
|
|
16888
16907
|
this.lastSeqByType.set(dataType, earliest - 1 & 65535);
|
|
@@ -17519,7 +17538,8 @@ var AccessUnitAssembler = class {
|
|
|
17519
17538
|
* `decode` extracts the payload of a frame, whatever the stream's encryption: it is called for EVERY
|
|
17520
17539
|
* frame, including a continuation, because a continuation carries its own wrapped key ahead of its share
|
|
17521
17540
|
* of the payload (measured: 129 bytes beyond what its header declares, exactly as a unit's first frame
|
|
17522
|
-
* carries).
|
|
17541
|
+
* carries). A continuation without its matching open unit has no Annex-B start code and cannot be emitted
|
|
17542
|
+
* as a complete unit, even if its repeated header says keyframe.
|
|
17523
17543
|
*/
|
|
17524
17544
|
push(payload, decode) {
|
|
17525
17545
|
const header = parseVideoFrameHeader(payload);
|
|
@@ -17539,6 +17559,8 @@ var AccessUnitAssembler = class {
|
|
|
17539
17559
|
return [unitOf(open2.header, Buffer.concat(open2.chunks))];
|
|
17540
17560
|
}
|
|
17541
17561
|
this.discard();
|
|
17562
|
+
if (!beginsAccessUnit(body))
|
|
17563
|
+
return [];
|
|
17542
17564
|
if (full) {
|
|
17543
17565
|
this.open = { header, chunks: [body], carried: body.length };
|
|
17544
17566
|
return [];
|
|
@@ -17892,6 +17914,12 @@ var LiveStream = class extends EventEmitter3 {
|
|
|
17892
17914
|
lastCodec = "h264";
|
|
17893
17915
|
/** Rebuilds an access unit the station split across several frames — see {@link AccessUnitAssembler}. */
|
|
17894
17916
|
units = new AccessUnitAssembler((drop) => this.reportDroppedUnit(drop));
|
|
17917
|
+
/** Whether a missing video datagram or incomplete unit invalidated the current reference chain. */
|
|
17918
|
+
awaitingKeyframeAfterGap = false;
|
|
17919
|
+
videoGapHandler = () => {
|
|
17920
|
+
this.units.discard();
|
|
17921
|
+
this.awaitingKeyframeAfterGap = true;
|
|
17922
|
+
};
|
|
17895
17923
|
/** The channel inbound media must be tagged with, once {@link acceptsMedia} trusts the station's tag. */
|
|
17896
17924
|
mediaChannel;
|
|
17897
17925
|
tracedFirstVideoCommand = false;
|
|
@@ -17934,6 +17962,7 @@ var LiveStream = class extends EventEmitter3 {
|
|
|
17934
17962
|
return this;
|
|
17935
17963
|
this.listening = true;
|
|
17936
17964
|
this.session.on("data", this.handler);
|
|
17965
|
+
this.session.on("videoGap", this.videoGapHandler);
|
|
17937
17966
|
this.session.on("liveStartUnacknowledged", this.unackedHandler);
|
|
17938
17967
|
this.sendStart();
|
|
17939
17968
|
const keepAliveMs = this.opts.keepAliveMs ?? DEFAULT_KEEPALIVE_MS;
|
|
@@ -18031,6 +18060,8 @@ var LiveStream = class extends EventEmitter3 {
|
|
|
18031
18060
|
return;
|
|
18032
18061
|
this.listening = false;
|
|
18033
18062
|
this.session.off("data", this.handler);
|
|
18063
|
+
this.session.off("videoGap", this.videoGapHandler);
|
|
18064
|
+
this.awaitingKeyframeAfterGap = false;
|
|
18034
18065
|
this.session.off("liveStartUnacknowledged", this.unackedHandler);
|
|
18035
18066
|
if (this.kaTimer)
|
|
18036
18067
|
clearInterval(this.kaTimer);
|
|
@@ -18066,6 +18097,11 @@ var LiveStream = class extends EventEmitter3 {
|
|
|
18066
18097
|
try {
|
|
18067
18098
|
if (f.commandId === CMD_VIDEO_FRAME2) {
|
|
18068
18099
|
for (const unit of this.units.push(f.data, (payload) => this.annexbOf(payload, f.signCode))) {
|
|
18100
|
+
if (this.awaitingKeyframeAfterGap) {
|
|
18101
|
+
if (!unit.keyframe)
|
|
18102
|
+
continue;
|
|
18103
|
+
this.awaitingKeyframeAfterGap = false;
|
|
18104
|
+
}
|
|
18069
18105
|
if (!this.tracedFirstVideoUnit) {
|
|
18070
18106
|
this.tracedFirstVideoUnit = true;
|
|
18071
18107
|
this.trace({ phase: "first-video-unit", keyframe: unit.keyframe });
|
|
@@ -18160,6 +18196,7 @@ var LiveStream = class extends EventEmitter3 {
|
|
|
18160
18196
|
* otherwise silent in both directions: no frame reaches a consumer, and nothing states why.
|
|
18161
18197
|
*/
|
|
18162
18198
|
reportDroppedUnit(drop) {
|
|
18199
|
+
this.awaitingKeyframeAfterGap = true;
|
|
18163
18200
|
const message = `[live] dropped an incomplete access unit (${drop.carried} bytes in ${drop.chunks} frame(s), tail never arrived, ${drop.count} so far)`;
|
|
18164
18201
|
if (drop.count === 1)
|
|
18165
18202
|
this.logger.warn(message);
|
|
@@ -21175,14 +21212,20 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21175
21212
|
}
|
|
21176
21213
|
}
|
|
21177
21214
|
/**
|
|
21178
|
-
* Restart
|
|
21179
|
-
*
|
|
21180
|
-
*
|
|
21181
|
-
*
|
|
21182
|
-
*
|
|
21215
|
+
* Restart the station that owns `sn`'s P2P session. `RESTART_HUB` (1034) is a station-scalar on the
|
|
21216
|
+
* broadcast channel 255 whose body is `[u32 value][account_id padded]`, value `0`. ✅ Wire-confirmed
|
|
21217
|
+
* byte-exact as a level-2 frame from a capture of the app's own HomeBase Restart (2026-08-03).
|
|
21218
|
+
*
|
|
21219
|
+
* The seal follows the session, as {@link sendBySessionLevel} defines it: a keyed session takes the
|
|
21220
|
+
* level-2 frame, a keyless one — a standalone camera, which never negotiates a key — the same body
|
|
21221
|
+
* sealed level-1. Both are replayed {@link DIRECT_CMD_SENDS}× at 200ms, as every other unacknowledged
|
|
21222
|
+
* control here is.
|
|
21183
21223
|
*/
|
|
21184
21224
|
async rebootStation(sn) {
|
|
21185
|
-
await this.
|
|
21225
|
+
await this.sendBySessionLevel(sn, {
|
|
21226
|
+
l2: (resolved) => this.replayLevel2Send(sn, `reboot ${sn}`, ({ session, accountId }) => session.sendRawLevel2Bytes(buildDirectBinaryBody(0, accountId), 255, P2P_ENVELOPE.RESTART_HUB, 8), resolved),
|
|
21227
|
+
l1: (resolved) => this.replayLevel2Send(sn, `reboot ${sn}`, ({ session, accountId }) => session.sendRawLevel1Bytes(buildDirectBinaryBody(0, accountId), 255, P2P_ENVELOPE.RESTART_HUB), resolved)
|
|
21228
|
+
});
|
|
21186
21229
|
}
|
|
21187
21230
|
/**
|
|
21188
21231
|
* A {@link MediaProvider} bound to one serial — resolves the session then calls `p2p/media`.
|
|
@@ -27288,16 +27331,20 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
27288
27331
|
await this.commandSinkFor(sn).dispatch(cmd);
|
|
27289
27332
|
}
|
|
27290
27333
|
/**
|
|
27291
|
-
* Restart a HomeBase.
|
|
27334
|
+
* Restart a station: a HomeBase, or a camera-family device that is its own station.
|
|
27292
27335
|
*
|
|
27293
|
-
*
|
|
27294
|
-
*
|
|
27295
|
-
*
|
|
27336
|
+
* The restart is `RESTART_HUB` on the station's broadcast channel, so it reaches whatever owns the
|
|
27337
|
+
* P2P session. A standalone camera or doorbell owns its own session and is the station it restarts.
|
|
27338
|
+
* A camera attached to a HomeBase is not — the same frame would restart its HomeBase — so its serial
|
|
27339
|
+
* throws, as does any non-camera serial, rather than doing nothing. The device drops its connection
|
|
27340
|
+
* and returns after a minute or two; a HomeBase takes everything behind it offline meanwhile.
|
|
27341
|
+
* Verified on real hardware against a HomeBase and a standalone indoor camera.
|
|
27296
27342
|
*/
|
|
27297
27343
|
async reboot(sn) {
|
|
27298
27344
|
const ctx = await this.commandContext(sn);
|
|
27299
|
-
|
|
27300
|
-
|
|
27345
|
+
const standaloneCamera = ctx.codec === "camera" && this.p2p.stationKeyOf(sn) === sn;
|
|
27346
|
+
if (!isHomeBase({ deviceType: ctx.deviceType, model: ctx.model }) && !standaloneCamera) {
|
|
27347
|
+
throw new Error(`reboot: ${sn} is neither a HomeBase nor a standalone camera (deviceType ${ctx.deviceType ?? "?"}, model ${ctx.model ?? "?"}) \u2014 restart addresses the station that owns the P2P session`);
|
|
27301
27348
|
}
|
|
27302
27349
|
await this.p2p.rebootStation(sn);
|
|
27303
27350
|
}
|