@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.
@@ -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 key the recording was stored under could not be obtained.
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 when the
174
- * recording carried none that could be decoded). `frames` counts the distinct video sequences emitted.
175
- * `missingFrames` counts gaps between received sequences and received sequences that yielded no usable
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, identified by the recording name and cipher id of
888
- * the event. One download runs at a time per station. Rejects with {@link RecordingDownloadError}.
889
- * Optional, and absent on a camera whose station's recording download is unverified.
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 === STATION_CHUNK_BYTES;
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
- if (!/^(?:h265_)?\d{14}$/.test(recording) || !Number.isInteger(channel) || channel < 0 || channel > 99)
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 `/media/mmcblk0p1/Camera${String(channel).padStart(2, "0")}/${recording}.dat`;
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 !== FINISH_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 decodeRecording(frames, eccPrivateKeyHex) {
20106
- const eccPrivateKey = Buffer.from(eccPrivateKeyHex, "hex");
20107
- if (eccPrivateKey.length !== 32)
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
- const decoder = new VideoFrameDecoder(eccPrivateKey);
20110
- const assembler = new AccessUnitAssembler();
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.length >= VIDEO_HEADER_LEN2 + header.payloadLength ? payload.subarray(VIDEO_HEADER_LEN2, VIDEO_HEADER_LEN2 + header.payloadLength) : void 0);
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
- const mediaKey = decoder.currentMediaKey;
20133
- if (!mediaKey || raw[5] !== AUDIO_CODEC_AAC_LC)
20156
+ if (raw[5] !== AUDIO_CODEC_AAC_LC)
20134
20157
  continue;
20135
- const aac = openSealedAudio(raw, mediaKey);
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 recordings = this.downloadsRecordings(sn) ? { downloadRecording: (opts) => this.downloadRecording(sn, opts) } : {};
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
- * Whether `sn` is a camera attached to a HomeBase 2 (T8010), the one station whose recording path layout
22910
- * and frame formats are confirmed.
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
- downloadsRecordings(sn) {
22938
+ recordingStationFor(sn) {
22913
22939
  const dev = this.recordFor(sn);
22914
22940
  if (!dev)
22915
- return false;
22941
+ return void 0;
22916
22942
  const parentSn = stationOf(dev);
22917
- return parentSn !== sn && this.recordFor(parentSn)?.model === "T8010";
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 a HomeBase 2 holds for an attached camera, then decode it
22921
- * ({@link decodeRecording}). Downloads queue per station: the station streams one recording at a time
22922
- * on the camera's channel, so a download starts only once the previous one has drained
22923
- * ({@link RecordingTransfer.drained}), even when that one's caller already has its answer. An abort
22924
- * while waiting rejects without sending anything; an abort mid-transfer rejects at once and the station's
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 = homeBase2RecordingPath(channel, opts.recording);
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 { recording: transfer.frames.then((frames) => decodeRecording(frames, eccKey)), drained: transfer.drained };
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.