@mega-yfue/eufy-sdk 0.6.0-beta.33 → 0.6.0-beta.35

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.
@@ -156,7 +156,7 @@ export declare class DeviceChannelUnresolvedError extends Error {
156
156
  * - `invalid-recording` — the recording name is not one the camera's station could hold.
157
157
  * - `key-unavailable` — the station stores the recording sealed and the key it was sealed under could not be
158
158
  * obtained.
159
- * - `no-data` — the station sent nothing within the wait.
159
+ * - `no-data` — the station sent no recording: it answered that it holds none, or sent nothing within the wait.
160
160
  * - `incomplete` — the transfer ended before the station's finish frame.
161
161
  * - `undecodable` — data arrived but no video frame could be decoded from it.
162
162
  */
@@ -172,13 +172,15 @@ export declare class RecordingDownloadError extends Error {
172
172
  * A recording downloaded from the station, as elementary streams.
173
173
  *
174
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
175
+ * when the recording carried none that could be decoded). `codec` is the codec of `video`, read off the parameter
176
+ * sets of its first keyframe that carries them, and is absent when no keyframe does. `frames` counts the distinct
177
+ * video sequences emitted. `missingFrames` counts gaps between received sequences and received sequences that yielded no
177
178
  * usable picture, including at either end. `durationMs` spans the first to the last emitted picture by the
178
179
  * camera's own timestamps; `fps` is derived from those stamps.
179
180
  */
180
181
  export interface RecordingDownload {
181
182
  video: Buffer;
183
+ codec?: VideoCodec;
182
184
  audio?: Buffer;
183
185
  frames: number;
184
186
  missingFrames: number;
@@ -493,7 +495,7 @@ export interface TuyaDpInbound {
493
495
  */
494
496
  onDps(sn: string, dps: Record<number, boolean | number | string>): void;
495
497
  }
496
- /** Elementary-stream video codec of a {@link LiveVideoFrame} — eufy cameras stream H.264 or H.265. */
498
+ /** Elementary-stream video codec — eufy cameras stream H.264 or H.265. */
497
499
  export type VideoCodec = "h264" | "h265" | "av1";
498
500
  /**
499
501
  * One decoded video access unit, as Annex-B (H.264 or H.265).
@@ -745,9 +747,12 @@ export interface LiveStreamConsumer extends LiveStreamHandle {
745
747
  export interface AbortableCall {
746
748
  signal?: AbortSignal;
747
749
  }
748
- /** Select an optical view; absence selects view 0. */
750
+ /** NVR output size requested when a live pull starts. */
751
+ export type MediaOutputResolution = "1080p";
752
+ /** Select an optical view and output size; absence selects view 0 at the route's default output. */
749
753
  export interface MediaSourceSelection {
750
754
  view?: number;
755
+ outputResolution?: MediaOutputResolution;
751
756
  }
752
757
  export interface SharedSourceHints {
753
758
  /**
@@ -774,6 +779,8 @@ export interface SharedSourceHints {
774
779
  * because an unbound model has no live client (hence `?.` at the call site).
775
780
  */
776
781
  export interface MediaProvider {
782
+ /** Output sizes supported by this bound media route. Absence means no size selection. */
783
+ outputResolutions?: readonly MediaOutputResolution[];
777
784
  /** Optical view identifiers supported by this bound media route. */
778
785
  views?: readonly number[];
779
786
  /**
package/dist/index.js CHANGED
@@ -3743,6 +3743,7 @@ var isNvrT8N00 = (ctx) => ctx.model === "T8N00";
3743
3743
  var NVR_MEDIA_CAMERA_MODELS = /* @__PURE__ */ new Set(["T8E00", "T8P00", "T8P10", "T8162", "T8410", "T8214"]);
3744
3744
  var isNvrMediaCamera = (ctx) => ctx.model !== void 0 && NVR_MEDIA_CAMERA_MODELS.has(ctx.model);
3745
3745
  var isNvrDualLensCamera = (ctx) => ctx.model === "T8E00";
3746
+ var isNvrFullHdCamera = (ctx) => ctx.model === "T8E00" || ctx.model === "T8P00";
3746
3747
  var isCallDoorbell = (ctx) => ctx.model === "T8223" || ctx.model === "T8224";
3747
3748
  var isAiotVacuum = (ctx) => ctx.category !== "eufy_home_tuya";
3748
3749
  var isTuyaVacuum = (ctx) => ctx.category === "eufy_home_tuya";
@@ -6084,10 +6085,12 @@ function refuseWhenDisabled(ctx, read) {
6084
6085
  if (CAMERA_MEMBERS.enabled.readReflectsWrite(ctx) && read("enabled")?.value === false)
6085
6086
  throw new CameraDisabledError(ctx.name ?? ctx.serial);
6086
6087
  }
6087
- function refuseUnsupportedView(media, opts) {
6088
+ function refuseUnsupportedSelection(media, opts) {
6088
6089
  const view = opts?.view ?? 0;
6089
6090
  if (!(media.views ?? [0]).includes(view))
6090
6091
  throw new RangeError(`camera: unsupported optical view ${view}`);
6092
+ if (opts?.outputResolution !== void 0 && !media.outputResolutions?.includes(opts.outputResolution))
6093
+ throw new RangeError(`camera: unsupported output resolution ${opts.outputResolution}`);
6091
6094
  }
6092
6095
  var CAMERA_MEMBERS = {
6093
6096
  /** Reads the latest cached report and exposes no setter. */
@@ -6376,6 +6379,7 @@ var CAMERA_MEMBERS = {
6376
6379
  write: (v, ctx) => statusLedCommand(asBool(v), ctx)
6377
6380
  },
6378
6381
  views: provided("media", (m) => m.views && (() => m.views), "Optical view identifiers supported by this camera's media route."),
6382
+ outputResolutions: provided("media", (m) => m.outputResolutions && (() => m.outputResolutions), "Output sizes supported by this camera's live media route; omission preserves its default output."),
6379
6383
  /**
6380
6384
  * Media is not a property: it returns DATA rather than moving state, its options are richer than a
6381
6385
  * value, and it exists only on a device bound to a provider. Each is declared once, with its
@@ -6396,29 +6400,30 @@ var CAMERA_MEMBERS = {
6396
6400
  snapshotStored: provided("media", (m, { ctx }) => isCameraCodec(ctx) && m.snapshotStored && (() => m.snapshotStored()), "Latest validated push thumbnail retained in memory.", ["snapshot"]),
6397
6401
  snapshotLive: provided("media", (m, { ctx, read }) => isCameraCodec(ctx) && (async (opts) => {
6398
6402
  refuseWhenDisabled(ctx, read);
6399
- refuseUnsupportedView(m, opts);
6403
+ refuseUnsupportedSelection(m, opts);
6400
6404
  return m.snapshotLive({ powered: poweredOf(ctx), ...opts });
6401
6405
  }), "Fresh still decoded from a short live burst."),
6402
6406
  live: provided("media", (m, { ctx, read }) => async (opts) => {
6403
6407
  refuseWhenDisabled(ctx, read);
6404
- refuseUnsupportedView(m, opts);
6408
+ refuseUnsupportedSelection(m, opts);
6405
6409
  return m.live({ powered: poweredOf(ctx), ...opts });
6406
6410
  }, "Open a managed live stream."),
6407
6411
  record: provided("media", (m, { ctx, read }) => isCameraCodec(ctx) && (async (...args) => {
6408
6412
  refuseWhenDisabled(ctx, read);
6409
6413
  return m.record(...args);
6410
6414
  }), "Record N seconds \u2192 an mp4/h264 buffer."),
6411
- /** The T8530 serves its live feed as a Readable too; its other media pulls stay withheld. */
6415
+ /** The T8530 serves its live feed as a Readable too. */
6412
6416
  openReadable: provided("media", (m, { ctx, read }) => (isCameraCodec(ctx) || isVideoSmartLock(ctx)) && m.openReadable && (async (opts) => {
6413
6417
  refuseWhenDisabled(ctx, read);
6414
- refuseUnsupportedView(m, opts);
6418
+ refuseUnsupportedSelection(m, opts);
6415
6419
  return m.openReadable({ powered: poweredOf(ctx), ...opts });
6416
6420
  }), "Open a node:stream Readable of the live feed."),
6417
6421
  recordFragments: provided("media", (m, { ctx, read }) => isCameraCodec(ctx) && m.recordFragments && ((opts) => {
6418
6422
  refuseWhenDisabled(ctx, read);
6419
6423
  return m.recordFragments({ powered: poweredOf(ctx), ...opts });
6420
6424
  }), "Continuous fragmented-MP4 (CMAF) recording."),
6421
- downloadRecording: provided("media", (m, { ctx }) => isCameraCodec(ctx) && m.downloadRecording && ((opts) => m.downloadRecording(opts)), "Download a recording the station holds \u2192 H.264 and AAC elementary streams."),
6425
+ /** A T8530 is offered the download behind a HomeBase 3 alone. */
6426
+ downloadRecording: provided("media", (m, { ctx }) => (isCameraCodec(ctx) || isVideoSmartLock(ctx) && ctx.stationSerial?.startsWith("T8030") === true) && m.downloadRecording && ((opts) => m.downloadRecording(opts)), "Download a recording the station holds \u2192 H.264 or H.265 and AAC elementary streams."),
6422
6427
  /**
6423
6428
  * Push audio from the host to this camera's speaker. Gated on the **speaker** param specifically —
6424
6429
  * not the `audio` capability, which resolves on a microphone alone and would advertise a speaker the
@@ -20057,7 +20062,16 @@ function homeBase3RecordingPath(channel, recording) {
20057
20062
  }
20058
20063
  var RECORDING_STATIONS = /* @__PURE__ */ new Map([
20059
20064
  ["T8010", { path: homeBase2RecordingPath, sealed: true, finishChannel: 0, chunkBytes: STATION_CHUNK_BYTES }],
20060
- ["T8030", { path: homeBase3RecordingPath, sealed: false, finishChannel: STATION_CHANNEL3, chunkBytes: 4e5 }]
20065
+ [
20066
+ "T8030",
20067
+ {
20068
+ path: homeBase3RecordingPath,
20069
+ sealed: false,
20070
+ finishChannel: STATION_CHANNEL3,
20071
+ chunkBytes: 4e5,
20072
+ missingCode: -111
20073
+ }
20074
+ ]
20061
20075
  ]);
20062
20076
  function receiveRecording(session, request) {
20063
20077
  let resolveFrames;
@@ -20101,6 +20115,14 @@ function receiveRecording(session, request) {
20101
20115
  collected = [];
20102
20116
  };
20103
20117
  const onData = (frame) => {
20118
+ if (frame.commandId === CommandType.CMD_DOWNLOAD_VIDEO) {
20119
+ if (frame.channel !== request.channel || frame.dataType !== 0 || received || draining)
20120
+ return;
20121
+ if (frame.data.length < 4 || frame.data.readInt32LE(0) !== request.missingCode)
20122
+ return;
20123
+ deliver(new RecordingDownloadError("no-data", `the station holds no recording at ${request.path}`));
20124
+ return stop();
20125
+ }
20104
20126
  if (frame.commandId === CommandType.CMD_DOWNLOAD_FINISH) {
20105
20127
  if (frame.channel !== request.channel && frame.channel !== request.finishChannel)
20106
20128
  return;
@@ -20171,6 +20193,7 @@ function decodeRecording(frames, station, eccPrivateKeyHex) {
20171
20193
  const decoder = eccPrivateKey && new VideoFrameDecoder(eccPrivateKey);
20172
20194
  const assembler = new AccessUnitAssembler(void 0, station.chunkBytes);
20173
20195
  const video = [];
20196
+ let codec;
20174
20197
  const audio = [];
20175
20198
  const numbers = /* @__PURE__ */ new Set();
20176
20199
  const receivedNumbers = /* @__PURE__ */ new Set();
@@ -20185,6 +20208,8 @@ function decodeRecording(frames, station, eccPrivateKeyHex) {
20185
20208
  const units = assembler.push(raw, (payload) => decoder && header.keyframe ? decoder.decodeFrame(payload)?.h264 : clearVideoPayload(payload, header));
20186
20209
  for (const unit of units) {
20187
20210
  video.push(unit.data);
20211
+ if (unit.keyframe)
20212
+ codec ??= sniffAnnexbCodec(unit.data);
20188
20213
  numbers.add(header.sequence);
20189
20214
  const stamp = raw.readUIntLE(14, 6);
20190
20215
  firstStamp ??= stamp;
@@ -20213,6 +20238,7 @@ function decodeRecording(frames, station, eccPrivateKeyHex) {
20213
20238
  const durationMs = (lastStamp ?? 0) - (firstStamp ?? 0);
20214
20239
  return {
20215
20240
  video: Buffer.concat(video),
20241
+ ...codec ? { codec } : {},
20216
20242
  ...audio.length ? { audio: Buffer.concat(audio) } : {},
20217
20243
  frames: ordered.length,
20218
20244
  missingFrames,
@@ -23027,6 +23053,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
23027
23053
  accountId,
23028
23054
  channel,
23029
23055
  finishChannel: station.finishChannel,
23056
+ missingCode: station.missingCode,
23030
23057
  timeoutMs: opts.timeoutMs,
23031
23058
  signal: opts.signal
23032
23059
  });
@@ -24477,6 +24504,7 @@ var RtcLiveStream = class extends EventEmitter7 {
24477
24504
  audio_chn: nvr ? this.opts.channel : -1,
24478
24505
  streamtype: 2,
24479
24506
  stitch_mode: 1,
24507
+ ...this.opts.outputResolution === "1080p" ? { stitch_resolution: { width: 1920, height: 1080 } } : {},
24480
24508
  chn_list: [{ index: 0, chn: this.opts.channel, sensor: this.opts.view ?? 0, isUps: 0, isClicked: true }],
24481
24509
  pip_cord: { x1: 0, y1: 0, x2: 0, y2: 0 },
24482
24510
  station_video_type: 6,
@@ -25878,9 +25906,9 @@ var RtcCommandRouter = class {
25878
25906
  return this.callMediaProviderFor(route, route.stationSn, (session) => new RtcCallLiveStream(session, this.deps.logger), `record is not available for ${route.stationSn} over a portal call`, 45e3);
25879
25907
  }
25880
25908
  /** NVR media uses a dedicated call per camera, independent of station commands. */
25881
- nvrMediaProviderFor(route, views) {
25909
+ nvrMediaProviderFor(route, views, outputResolutions) {
25882
25910
  return {
25883
- ...this.callMediaProviderFor(route, route.cameraSn, (session, accountId, view) => new RtcLiveStream({
25911
+ ...this.callMediaProviderFor(route, route.cameraSn, (session, accountId, view, outputResolution) => new RtcLiveStream({
25884
25912
  session,
25885
25913
  seg: new SegmentCounter(),
25886
25914
  stationSn: route.stationSn,
@@ -25888,9 +25916,11 @@ var RtcCommandRouter = class {
25888
25916
  accountId,
25889
25917
  logger: this.deps.logger,
25890
25918
  nvr: true,
25891
- view
25919
+ view,
25920
+ outputResolution
25892
25921
  }), "record is not available over an NVR portal call"),
25893
- views
25922
+ views,
25923
+ outputResolutions
25894
25924
  };
25895
25925
  }
25896
25926
  /**
@@ -25904,7 +25934,8 @@ var RtcCommandRouter = class {
25904
25934
  const sourceFor = (opts = {}) => {
25905
25935
  opts.signal?.throwIfAborted();
25906
25936
  const view = opts.view ?? 0;
25907
- const sourceKey = view === 0 ? key : `${key}:view:${view}`;
25937
+ const viewKey = view === 0 ? key : `${key}:view:${view}`;
25938
+ const sourceKey = opts.outputResolution ? `${viewKey}:output:${opts.outputResolution}` : viewKey;
25908
25939
  let source = this.callSources.get(sourceKey);
25909
25940
  if (!source) {
25910
25941
  source = new SharedLiveSource({
@@ -25913,7 +25944,7 @@ var RtcCommandRouter = class {
25913
25944
  if (!identity)
25914
25945
  throw new Error(`rtc: not logged in, cannot call ${route.stationSn}`);
25915
25946
  const accountId = route.adminUserId || identity.userId;
25916
- return makeStream(this.newSession(route, accountId, identity, this.callShards.get(key)), accountId, view);
25947
+ return makeStream(this.newSession(route, accountId, identity, this.callShards.get(key)), accountId, view, opts.outputResolution);
25917
25948
  },
25918
25949
  label: sourceKey,
25919
25950
  logger: this.deps.logger,
@@ -30764,7 +30795,7 @@ var EufyMega = class _EufyMega extends EventEmitter14 {
30764
30795
  if (route.signalingMode === "call") {
30765
30796
  if (!camera || !isNvrMediaCamera(camera))
30766
30797
  return void 0;
30767
- return this.rtc.nvrMediaProviderFor({ ...route, cameraSn: sn, channel }, isNvrDualLensCamera(camera) ? [0, 1] : [0]);
30798
+ return this.rtc.nvrMediaProviderFor({ ...route, cameraSn: sn, channel }, isNvrDualLensCamera(camera) ? [0, 1] : [0], isNvrFullHdCamera(camera) ? ["1080p"] : void 0);
30768
30799
  }
30769
30800
  return this.rtc.mediaProviderFor({ ...route, cameraSn: sn, channel });
30770
30801
  }