@mega-yfue/eufy-sdk 0.6.0-beta.30 → 0.6.0-beta.31
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
|
@@ -153,8 +153,9 @@ export declare class DeviceChannelUnresolvedError extends Error {
|
|
|
153
153
|
/**
|
|
154
154
|
* Why {@link MediaProvider.downloadRecording} returned no recording.
|
|
155
155
|
*
|
|
156
|
-
* - `invalid-recording` — the recording name is not one the station could hold.
|
|
157
|
-
* - `key-unavailable` — the
|
|
156
|
+
* - `invalid-recording` — the recording name is not one the camera's station could hold.
|
|
157
|
+
* - `key-unavailable` — the station stores the recording sealed and the key it was sealed under could not be
|
|
158
|
+
* obtained.
|
|
158
159
|
* - `no-data` — the station sent nothing within the wait.
|
|
159
160
|
* - `incomplete` — the transfer ended before the station's finish frame.
|
|
160
161
|
* - `undecodable` — data arrived but no video frame could be decoded from it.
|
|
@@ -170,10 +171,10 @@ export declare class RecordingDownloadError extends Error {
|
|
|
170
171
|
/**
|
|
171
172
|
* A recording downloaded from the station, as elementary streams.
|
|
172
173
|
*
|
|
173
|
-
* `video` is Annex-B H.264 in decode order, `audio` AAC-LC 16 kHz mono in ADTS framing (absent
|
|
174
|
-
* recording carried none that could be decoded). `frames` counts the distinct video sequences
|
|
175
|
-
* `missingFrames` counts gaps between received sequences and received sequences that yielded no
|
|
176
|
-
* picture, including at either end. `durationMs` spans the first to the last emitted picture by the
|
|
174
|
+
* `video` is Annex-B H.264 or H.265 in decode order, `audio` AAC-LC 16 kHz mono in ADTS framing (absent
|
|
175
|
+
* when the recording carried none that could be decoded). `frames` counts the distinct video sequences
|
|
176
|
+
* emitted. `missingFrames` counts gaps between received sequences and received sequences that yielded no
|
|
177
|
+
* usable picture, including at either end. `durationMs` spans the first to the last emitted picture by the
|
|
177
178
|
* camera's own timestamps; `fps` is derived from those stamps.
|
|
178
179
|
*/
|
|
179
180
|
export interface RecordingDownload {
|
|
@@ -884,9 +885,11 @@ export interface MediaProvider {
|
|
|
884
885
|
encoder?: AacEncoder;
|
|
885
886
|
} & SharedSourceHints): Promise<TalkbackHandle>;
|
|
886
887
|
/**
|
|
887
|
-
* Download one recording the camera's station stored
|
|
888
|
-
* the
|
|
889
|
-
*
|
|
888
|
+
* Download one recording the camera's station stored. `recording` names it: the file name the station stores
|
|
889
|
+
* it under, without the folder or the extension. `cipherId` is the cipher the station names for it, and is not
|
|
890
|
+
* read on a station that stores its recordings in the clear. One download runs at a time per station. Rejects with
|
|
891
|
+
* {@link RecordingDownloadError}. Optional, and absent on a camera whose station's recording download is
|
|
892
|
+
* unverified.
|
|
890
893
|
*/
|
|
891
894
|
downloadRecording?(opts: {
|
|
892
895
|
recording: string;
|
package/dist/index.js
CHANGED
|
@@ -18951,15 +18951,19 @@ var VideoFrameDecoder = class {
|
|
|
18951
18951
|
var STATION_CHUNK_BYTES = 64e3;
|
|
18952
18952
|
var AccessUnitAssembler = class {
|
|
18953
18953
|
onDropped;
|
|
18954
|
+
chunkBytes;
|
|
18954
18955
|
open;
|
|
18955
18956
|
droppedUnits = 0;
|
|
18956
18957
|
/**
|
|
18957
18958
|
* @param onDropped called for each incomplete unit discarded, with what had arrived, how many frames it
|
|
18958
18959
|
* arrived in, and how many this assembler has dropped in total — a running count, so no caller keeps
|
|
18959
18960
|
* its own.
|
|
18961
|
+
* @param chunkBytes the payload size the station fills a frame to before continuing a unit in the next. A
|
|
18962
|
+
* unit is held while its latest frame declares exactly this. {@link STATION_CHUNK_BYTES} when omitted.
|
|
18960
18963
|
*/
|
|
18961
|
-
constructor(onDropped) {
|
|
18964
|
+
constructor(onDropped, chunkBytes = STATION_CHUNK_BYTES) {
|
|
18962
18965
|
this.onDropped = onDropped;
|
|
18966
|
+
this.chunkBytes = chunkBytes;
|
|
18963
18967
|
}
|
|
18964
18968
|
/**
|
|
18965
18969
|
* Feed one raw `CMD_VIDEO_FRAME` payload; returns the access units it completed — none while a unit is
|
|
@@ -18978,7 +18982,7 @@ var AccessUnitAssembler = class {
|
|
|
18978
18982
|
this.discard();
|
|
18979
18983
|
return [];
|
|
18980
18984
|
}
|
|
18981
|
-
const full = header.payloadLength ===
|
|
18985
|
+
const full = header.payloadLength === this.chunkBytes;
|
|
18982
18986
|
const open2 = this.open;
|
|
18983
18987
|
if (open2 && sameUnit(open2.header, header) && !beginsAccessUnit(body)) {
|
|
18984
18988
|
open2.chunks.push(body);
|
|
@@ -19992,16 +19996,31 @@ function annexbToJpeg(annexb, spawn2) {
|
|
|
19992
19996
|
var RECORDING_DATA_TYPE = 3;
|
|
19993
19997
|
var VIDEO_HEADER_LEN2 = 22;
|
|
19994
19998
|
var AUDIO_CODEC_AAC_LC = 0;
|
|
19995
|
-
var FINISH_CHANNEL = 0;
|
|
19996
19999
|
var FIRST_FRAME_WAIT_MS = 2e4;
|
|
19997
20000
|
var IDLE_END_MS = 15e3;
|
|
19998
20001
|
var DEFAULT_TRANSFER_MS = 12e4;
|
|
19999
20002
|
var MAX_TRANSFER_BYTES = 32 * 1024 * 1024;
|
|
20003
|
+
function cameraFolder(channel) {
|
|
20004
|
+
if (!Number.isInteger(channel) || channel < 0 || channel > 99)
|
|
20005
|
+
return void 0;
|
|
20006
|
+
return `Camera${String(channel).padStart(2, "0")}`;
|
|
20007
|
+
}
|
|
20000
20008
|
function homeBase2RecordingPath(channel, recording) {
|
|
20001
|
-
|
|
20009
|
+
const folder = cameraFolder(channel);
|
|
20010
|
+
if (!folder || !/^(?:h265_)?\d{14}$/.test(recording))
|
|
20011
|
+
return void 0;
|
|
20012
|
+
return `/media/mmcblk0p1/${folder}/${recording}.dat`;
|
|
20013
|
+
}
|
|
20014
|
+
function homeBase3RecordingPath(channel, recording) {
|
|
20015
|
+
const folder = cameraFolder(channel);
|
|
20016
|
+
if (!folder || !/^\d{14}$/.test(recording))
|
|
20002
20017
|
return void 0;
|
|
20003
|
-
return `/
|
|
20018
|
+
return `/zx/emmcdata/${folder}/${recording.slice(0, 6)}/${recording}/${recording}.zxvideo`;
|
|
20004
20019
|
}
|
|
20020
|
+
var RECORDING_STATIONS = /* @__PURE__ */ new Map([
|
|
20021
|
+
["T8010", { path: homeBase2RecordingPath, sealed: true, finishChannel: 0, chunkBytes: STATION_CHUNK_BYTES }],
|
|
20022
|
+
["T8030", { path: homeBase3RecordingPath, sealed: false, finishChannel: STATION_CHANNEL3, chunkBytes: 4e5 }]
|
|
20023
|
+
]);
|
|
20005
20024
|
function receiveRecording(session, request) {
|
|
20006
20025
|
let resolveFrames;
|
|
20007
20026
|
let rejectFrames;
|
|
@@ -20045,7 +20064,7 @@ function receiveRecording(session, request) {
|
|
|
20045
20064
|
};
|
|
20046
20065
|
const onData = (frame) => {
|
|
20047
20066
|
if (frame.commandId === CommandType.CMD_DOWNLOAD_FINISH) {
|
|
20048
|
-
if (frame.channel !== request.channel && frame.channel !==
|
|
20067
|
+
if (frame.channel !== request.channel && frame.channel !== request.finishChannel)
|
|
20049
20068
|
return;
|
|
20050
20069
|
if (draining)
|
|
20051
20070
|
incomplete(draining);
|
|
@@ -20102,12 +20121,17 @@ function receiveRecording(session, request) {
|
|
|
20102
20121
|
}
|
|
20103
20122
|
return { frames, drained };
|
|
20104
20123
|
}
|
|
20105
|
-
function
|
|
20106
|
-
const
|
|
20107
|
-
|
|
20124
|
+
function clearVideoPayload(raw, header) {
|
|
20125
|
+
const end = VIDEO_HEADER_LEN2 + header.payloadLength;
|
|
20126
|
+
return raw.length >= end ? raw.subarray(VIDEO_HEADER_LEN2, end) : void 0;
|
|
20127
|
+
}
|
|
20128
|
+
function decodeRecording(frames, station, eccPrivateKeyHex) {
|
|
20129
|
+
const eccPrivateKey = station.sealed ? Buffer.from(eccPrivateKeyHex ?? "", "hex") : void 0;
|
|
20130
|
+
if (eccPrivateKey && eccPrivateKey.length !== 32) {
|
|
20108
20131
|
throw new RecordingDownloadError("undecodable", "the cipher key is not a P-256 key");
|
|
20109
|
-
|
|
20110
|
-
const
|
|
20132
|
+
}
|
|
20133
|
+
const decoder = eccPrivateKey && new VideoFrameDecoder(eccPrivateKey);
|
|
20134
|
+
const assembler = new AccessUnitAssembler(void 0, station.chunkBytes);
|
|
20111
20135
|
const video = [];
|
|
20112
20136
|
const audio = [];
|
|
20113
20137
|
const numbers = /* @__PURE__ */ new Set();
|
|
@@ -20120,7 +20144,7 @@ function decodeRecording(frames, eccPrivateKeyHex) {
|
|
|
20120
20144
|
if (!header)
|
|
20121
20145
|
continue;
|
|
20122
20146
|
receivedNumbers.add(header.sequence);
|
|
20123
|
-
const units = assembler.push(raw, (payload) => header.keyframe ? decoder.decodeFrame(payload)?.h264 : payload
|
|
20147
|
+
const units = assembler.push(raw, (payload) => decoder && header.keyframe ? decoder.decodeFrame(payload)?.h264 : clearVideoPayload(payload, header));
|
|
20124
20148
|
for (const unit of units) {
|
|
20125
20149
|
video.push(unit.data);
|
|
20126
20150
|
numbers.add(header.sequence);
|
|
@@ -20129,10 +20153,10 @@ function decodeRecording(frames, eccPrivateKeyHex) {
|
|
|
20129
20153
|
lastStamp = stamp;
|
|
20130
20154
|
}
|
|
20131
20155
|
} else if (commandId === CommandType.CMD_AUDIO_FRAME) {
|
|
20132
|
-
|
|
20133
|
-
if (!mediaKey || raw[5] !== AUDIO_CODEC_AAC_LC)
|
|
20156
|
+
if (raw[5] !== AUDIO_CODEC_AAC_LC)
|
|
20134
20157
|
continue;
|
|
20135
|
-
const
|
|
20158
|
+
const mediaKey = decoder?.currentMediaKey;
|
|
20159
|
+
const aac = decoder ? mediaKey && openSealedAudio(raw, mediaKey) : raw.subarray(AUDIO_HEADER_LEN);
|
|
20136
20160
|
if (aac?.length)
|
|
20137
20161
|
audio.push(aac);
|
|
20138
20162
|
}
|
|
@@ -22864,7 +22888,8 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22864
22888
|
* start has no level-1 form for.
|
|
22865
22889
|
*/
|
|
22866
22890
|
mediaProviderFor(sn) {
|
|
22867
|
-
const
|
|
22891
|
+
const station = this.recordingStationFor(sn);
|
|
22892
|
+
const recordings = station ? { downloadRecording: (opts) => this.downloadRecording(sn, station, opts) } : {};
|
|
22868
22893
|
return {
|
|
22869
22894
|
...recordings,
|
|
22870
22895
|
snapshotLive: async (opts) => {
|
|
@@ -22906,29 +22931,32 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22906
22931
|
};
|
|
22907
22932
|
}
|
|
22908
22933
|
/**
|
|
22909
|
-
*
|
|
22910
|
-
*
|
|
22934
|
+
* The {@link RecordingStation} of the station `sn` is attached to, when `sn` is a camera attached to a
|
|
22935
|
+
* station with an entry in {@link RECORDING_STATIONS}. A standalone device, or a camera on any other
|
|
22936
|
+
* station, answers `undefined`.
|
|
22911
22937
|
*/
|
|
22912
|
-
|
|
22938
|
+
recordingStationFor(sn) {
|
|
22913
22939
|
const dev = this.recordFor(sn);
|
|
22914
22940
|
if (!dev)
|
|
22915
|
-
return
|
|
22941
|
+
return void 0;
|
|
22916
22942
|
const parentSn = stationOf(dev);
|
|
22917
|
-
|
|
22943
|
+
if (parentSn === sn)
|
|
22944
|
+
return void 0;
|
|
22945
|
+
return RECORDING_STATIONS.get(this.recordFor(parentSn)?.model ?? "");
|
|
22918
22946
|
}
|
|
22919
22947
|
/**
|
|
22920
|
-
* Download one recording
|
|
22921
|
-
* ({@link decodeRecording}).
|
|
22922
|
-
* on the camera's channel, so a download starts only
|
|
22923
|
-
* ({@link RecordingTransfer.drained}), even when that one's caller already
|
|
22924
|
-
* while waiting rejects without sending anything; an abort mid-transfer rejects at
|
|
22925
|
-
* turn is held until the transfer drains.
|
|
22948
|
+
* Download one recording `station` holds for the attached camera `sn`, then decode it
|
|
22949
|
+
* ({@link decodeRecording}). `station` is the entry {@link mediaProviderFor} bound. Downloads queue per
|
|
22950
|
+
* station: the station streams one recording at a time on the camera's channel, so a download starts only
|
|
22951
|
+
* once the previous one has drained ({@link RecordingTransfer.drained}), even when that one's caller already
|
|
22952
|
+
* has its answer. An abort while waiting rejects without sending anything; an abort mid-transfer rejects at
|
|
22953
|
+
* once and the station's turn is held until the transfer drains.
|
|
22926
22954
|
*/
|
|
22927
|
-
async downloadRecording(sn, opts) {
|
|
22955
|
+
async downloadRecording(sn, station, opts) {
|
|
22928
22956
|
opts.signal?.throwIfAborted();
|
|
22929
22957
|
const parentSn = this.stationKeyOf(sn);
|
|
22930
22958
|
const previous = this.recordingDownloads.get(parentSn) ?? Promise.resolve();
|
|
22931
|
-
const started = this.waitTurn(previous, opts.signal).then(() => this.startRecordingDownload(sn, opts));
|
|
22959
|
+
const started = this.waitTurn(previous, opts.signal).then(() => this.startRecordingDownload(sn, station, opts));
|
|
22932
22960
|
const turn = Promise.allSettled([previous, started.then((t) => t.drained)]).then(() => {
|
|
22933
22961
|
if (this.recordingDownloads.get(parentSn) === turn)
|
|
22934
22962
|
this.recordingDownloads.delete(parentSn);
|
|
@@ -22942,25 +22970,32 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22942
22970
|
}
|
|
22943
22971
|
/**
|
|
22944
22972
|
* Start one recording download, once the station's previous download has drained: the decoded recording,
|
|
22945
|
-
* and when the station is done sending it.
|
|
22973
|
+
* and when the station is done sending it. The path is `station.path(channel, opts.recording)`, and a name
|
|
22974
|
+
* it refuses rejects `invalid-recording` before anything is sent. On a sealed station the cipher's ECC key
|
|
22975
|
+
* is looked up, and its absence rejects `key-unavailable` before anything is sent. A station in the clear
|
|
22976
|
+
* reads no cipher, and `opts.cipherId` is not read.
|
|
22946
22977
|
*/
|
|
22947
|
-
async startRecordingDownload(sn, opts) {
|
|
22978
|
+
async startRecordingDownload(sn, station, opts) {
|
|
22948
22979
|
const { session, parentSn, channel, accountId } = await this.resolveSession(sn, { signal: opts.signal });
|
|
22949
|
-
const path =
|
|
22980
|
+
const path = station.path(channel, opts.recording);
|
|
22950
22981
|
if (!path)
|
|
22951
22982
|
throw new RecordingDownloadError("invalid-recording", `no recording named ${opts.recording}`);
|
|
22952
|
-
const eccKey = (await this.cipherKeysFor(session, opts.cipherId, accountId, parentSn))?.ecc;
|
|
22953
|
-
if (!eccKey) {
|
|
22983
|
+
const eccKey = station.sealed ? (await this.cipherKeysFor(session, opts.cipherId, accountId, parentSn))?.ecc : void 0;
|
|
22984
|
+
if (station.sealed && !eccKey) {
|
|
22954
22985
|
throw new RecordingDownloadError("key-unavailable", `cipher ${opts.cipherId} could not be obtained`);
|
|
22955
22986
|
}
|
|
22956
22987
|
const transfer = receiveRecording(session, {
|
|
22957
22988
|
path,
|
|
22958
22989
|
accountId,
|
|
22959
22990
|
channel,
|
|
22991
|
+
finishChannel: station.finishChannel,
|
|
22960
22992
|
timeoutMs: opts.timeoutMs,
|
|
22961
22993
|
signal: opts.signal
|
|
22962
22994
|
});
|
|
22963
|
-
return {
|
|
22995
|
+
return {
|
|
22996
|
+
recording: transfer.frames.then((frames) => decodeRecording(frames, station, eccKey)),
|
|
22997
|
+
drained: transfer.drained
|
|
22998
|
+
};
|
|
22964
22999
|
}
|
|
22965
23000
|
/**
|
|
22966
23001
|
* Open a {@link Talkback} on a device's camera channel.
|