@mega-yfue/eufy-sdk 0.6.0-beta.34 → 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 +43 -13
- package/dist/index.js.map +2 -2
- package/dist/model/capabilities/camera.d.ts +1 -0
- 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,12 +6400,12 @@ 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) => {
|
|
@@ -6411,7 +6415,7 @@ var CAMERA_MEMBERS = {
|
|
|
6411
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) => {
|
|
@@ -6419,7 +6423,7 @@ var CAMERA_MEMBERS = {
|
|
|
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. */
|
|
6422
|
-
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 and AAC elementary streams."),
|
|
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."),
|
|
6423
6427
|
/**
|
|
6424
6428
|
* Push audio from the host to this camera's speaker. Gated on the **speaker** param specifically —
|
|
6425
6429
|
* not the `audio` capability, which resolves on a microphone alone and would advertise a speaker the
|
|
@@ -20058,7 +20062,16 @@ function homeBase3RecordingPath(channel, recording) {
|
|
|
20058
20062
|
}
|
|
20059
20063
|
var RECORDING_STATIONS = /* @__PURE__ */ new Map([
|
|
20060
20064
|
["T8010", { path: homeBase2RecordingPath, sealed: true, finishChannel: 0, chunkBytes: STATION_CHUNK_BYTES }],
|
|
20061
|
-
[
|
|
20065
|
+
[
|
|
20066
|
+
"T8030",
|
|
20067
|
+
{
|
|
20068
|
+
path: homeBase3RecordingPath,
|
|
20069
|
+
sealed: false,
|
|
20070
|
+
finishChannel: STATION_CHANNEL3,
|
|
20071
|
+
chunkBytes: 4e5,
|
|
20072
|
+
missingCode: -111
|
|
20073
|
+
}
|
|
20074
|
+
]
|
|
20062
20075
|
]);
|
|
20063
20076
|
function receiveRecording(session, request) {
|
|
20064
20077
|
let resolveFrames;
|
|
@@ -20102,6 +20115,14 @@ function receiveRecording(session, request) {
|
|
|
20102
20115
|
collected = [];
|
|
20103
20116
|
};
|
|
20104
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
|
+
}
|
|
20105
20126
|
if (frame.commandId === CommandType.CMD_DOWNLOAD_FINISH) {
|
|
20106
20127
|
if (frame.channel !== request.channel && frame.channel !== request.finishChannel)
|
|
20107
20128
|
return;
|
|
@@ -20172,6 +20193,7 @@ function decodeRecording(frames, station, eccPrivateKeyHex) {
|
|
|
20172
20193
|
const decoder = eccPrivateKey && new VideoFrameDecoder(eccPrivateKey);
|
|
20173
20194
|
const assembler = new AccessUnitAssembler(void 0, station.chunkBytes);
|
|
20174
20195
|
const video = [];
|
|
20196
|
+
let codec;
|
|
20175
20197
|
const audio = [];
|
|
20176
20198
|
const numbers = /* @__PURE__ */ new Set();
|
|
20177
20199
|
const receivedNumbers = /* @__PURE__ */ new Set();
|
|
@@ -20186,6 +20208,8 @@ function decodeRecording(frames, station, eccPrivateKeyHex) {
|
|
|
20186
20208
|
const units = assembler.push(raw, (payload) => decoder && header.keyframe ? decoder.decodeFrame(payload)?.h264 : clearVideoPayload(payload, header));
|
|
20187
20209
|
for (const unit of units) {
|
|
20188
20210
|
video.push(unit.data);
|
|
20211
|
+
if (unit.keyframe)
|
|
20212
|
+
codec ??= sniffAnnexbCodec(unit.data);
|
|
20189
20213
|
numbers.add(header.sequence);
|
|
20190
20214
|
const stamp = raw.readUIntLE(14, 6);
|
|
20191
20215
|
firstStamp ??= stamp;
|
|
@@ -20214,6 +20238,7 @@ function decodeRecording(frames, station, eccPrivateKeyHex) {
|
|
|
20214
20238
|
const durationMs = (lastStamp ?? 0) - (firstStamp ?? 0);
|
|
20215
20239
|
return {
|
|
20216
20240
|
video: Buffer.concat(video),
|
|
20241
|
+
...codec ? { codec } : {},
|
|
20217
20242
|
...audio.length ? { audio: Buffer.concat(audio) } : {},
|
|
20218
20243
|
frames: ordered.length,
|
|
20219
20244
|
missingFrames,
|
|
@@ -23028,6 +23053,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23028
23053
|
accountId,
|
|
23029
23054
|
channel,
|
|
23030
23055
|
finishChannel: station.finishChannel,
|
|
23056
|
+
missingCode: station.missingCode,
|
|
23031
23057
|
timeoutMs: opts.timeoutMs,
|
|
23032
23058
|
signal: opts.signal
|
|
23033
23059
|
});
|
|
@@ -24478,6 +24504,7 @@ var RtcLiveStream = class extends EventEmitter7 {
|
|
|
24478
24504
|
audio_chn: nvr ? this.opts.channel : -1,
|
|
24479
24505
|
streamtype: 2,
|
|
24480
24506
|
stitch_mode: 1,
|
|
24507
|
+
...this.opts.outputResolution === "1080p" ? { stitch_resolution: { width: 1920, height: 1080 } } : {},
|
|
24481
24508
|
chn_list: [{ index: 0, chn: this.opts.channel, sensor: this.opts.view ?? 0, isUps: 0, isClicked: true }],
|
|
24482
24509
|
pip_cord: { x1: 0, y1: 0, x2: 0, y2: 0 },
|
|
24483
24510
|
station_video_type: 6,
|
|
@@ -25879,9 +25906,9 @@ var RtcCommandRouter = class {
|
|
|
25879
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);
|
|
25880
25907
|
}
|
|
25881
25908
|
/** NVR media uses a dedicated call per camera, independent of station commands. */
|
|
25882
|
-
nvrMediaProviderFor(route, views) {
|
|
25909
|
+
nvrMediaProviderFor(route, views, outputResolutions) {
|
|
25883
25910
|
return {
|
|
25884
|
-
...this.callMediaProviderFor(route, route.cameraSn, (session, accountId, view) => new RtcLiveStream({
|
|
25911
|
+
...this.callMediaProviderFor(route, route.cameraSn, (session, accountId, view, outputResolution) => new RtcLiveStream({
|
|
25885
25912
|
session,
|
|
25886
25913
|
seg: new SegmentCounter(),
|
|
25887
25914
|
stationSn: route.stationSn,
|
|
@@ -25889,9 +25916,11 @@ var RtcCommandRouter = class {
|
|
|
25889
25916
|
accountId,
|
|
25890
25917
|
logger: this.deps.logger,
|
|
25891
25918
|
nvr: true,
|
|
25892
|
-
view
|
|
25919
|
+
view,
|
|
25920
|
+
outputResolution
|
|
25893
25921
|
}), "record is not available over an NVR portal call"),
|
|
25894
|
-
views
|
|
25922
|
+
views,
|
|
25923
|
+
outputResolutions
|
|
25895
25924
|
};
|
|
25896
25925
|
}
|
|
25897
25926
|
/**
|
|
@@ -25905,7 +25934,8 @@ var RtcCommandRouter = class {
|
|
|
25905
25934
|
const sourceFor = (opts = {}) => {
|
|
25906
25935
|
opts.signal?.throwIfAborted();
|
|
25907
25936
|
const view = opts.view ?? 0;
|
|
25908
|
-
const
|
|
25937
|
+
const viewKey = view === 0 ? key : `${key}:view:${view}`;
|
|
25938
|
+
const sourceKey = opts.outputResolution ? `${viewKey}:output:${opts.outputResolution}` : viewKey;
|
|
25909
25939
|
let source = this.callSources.get(sourceKey);
|
|
25910
25940
|
if (!source) {
|
|
25911
25941
|
source = new SharedLiveSource({
|
|
@@ -25914,7 +25944,7 @@ var RtcCommandRouter = class {
|
|
|
25914
25944
|
if (!identity)
|
|
25915
25945
|
throw new Error(`rtc: not logged in, cannot call ${route.stationSn}`);
|
|
25916
25946
|
const accountId = route.adminUserId || identity.userId;
|
|
25917
|
-
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);
|
|
25918
25948
|
},
|
|
25919
25949
|
label: sourceKey,
|
|
25920
25950
|
logger: this.deps.logger,
|
|
@@ -30765,7 +30795,7 @@ var EufyMega = class _EufyMega extends EventEmitter14 {
|
|
|
30765
30795
|
if (route.signalingMode === "call") {
|
|
30766
30796
|
if (!camera || !isNvrMediaCamera(camera))
|
|
30767
30797
|
return void 0;
|
|
30768
|
-
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);
|
|
30769
30799
|
}
|
|
30770
30800
|
return this.rtc.mediaProviderFor({ ...route, cameraSn: sn, channel });
|
|
30771
30801
|
}
|