@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.
- package/dist/core/contracts.d.ts +12 -5
- package/dist/index.js +45 -14
- package/dist/index.js.map +2 -2
- package/dist/model/capabilities/camera.d.ts +3 -1
- package/dist/model/device-family.d.ts +2 -0
- package/dist/transport/p2p/recording-download.d.ts +11 -2
- package/dist/transport/rtc/command-router.d.ts +2 -2
- package/dist/transport/rtc/live.d.ts +4 -1
- package/package.json +1 -1
package/dist/core/contracts.d.ts
CHANGED
|
@@ -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). `
|
|
176
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
|
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
|
}
|