@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.
|
@@ -314,24 +314,28 @@ export declare class P2PCommandRouter {
|
|
|
314
314
|
*/
|
|
315
315
|
mediaProviderFor(sn: string): MediaProvider;
|
|
316
316
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
317
|
+
* The {@link RecordingStation} of the station `sn` is attached to, when `sn` is a camera attached to a
|
|
318
|
+
* station with an entry in {@link RECORDING_STATIONS}. A standalone device, or a camera on any other
|
|
319
|
+
* station, answers `undefined`.
|
|
319
320
|
*/
|
|
320
|
-
private
|
|
321
|
+
private recordingStationFor;
|
|
321
322
|
/**
|
|
322
|
-
* Download one recording
|
|
323
|
-
* ({@link decodeRecording}).
|
|
324
|
-
* on the camera's channel, so a download starts only
|
|
325
|
-
* ({@link RecordingTransfer.drained}), even when that one's caller already
|
|
326
|
-
* while waiting rejects without sending anything; an abort mid-transfer rejects at
|
|
327
|
-
* turn is held until the transfer drains.
|
|
323
|
+
* Download one recording `station` holds for the attached camera `sn`, then decode it
|
|
324
|
+
* ({@link decodeRecording}). `station` is the entry {@link mediaProviderFor} bound. Downloads queue per
|
|
325
|
+
* station: the station streams one recording at a time on the camera's channel, so a download starts only
|
|
326
|
+
* once the previous one has drained ({@link RecordingTransfer.drained}), even when that one's caller already
|
|
327
|
+
* has its answer. An abort while waiting rejects without sending anything; an abort mid-transfer rejects at
|
|
328
|
+
* once and the station's turn is held until the transfer drains.
|
|
328
329
|
*/
|
|
329
330
|
private downloadRecording;
|
|
330
331
|
/** Resolve once `previous` settles, or reject as soon as `signal` aborts. */
|
|
331
332
|
private waitTurn;
|
|
332
333
|
/**
|
|
333
334
|
* Start one recording download, once the station's previous download has drained: the decoded recording,
|
|
334
|
-
* and when the station is done sending it.
|
|
335
|
+
* and when the station is done sending it. The path is `station.path(channel, opts.recording)`, and a name
|
|
336
|
+
* it refuses rejects `invalid-recording` before anything is sent. On a sealed station the cipher's ECC key
|
|
337
|
+
* is looked up, and its absence rejects `key-unavailable` before anything is sent. A station in the clear
|
|
338
|
+
* reads no cipher, and `opts.cipherId` is not read.
|
|
335
339
|
*/
|
|
336
340
|
private startRecordingDownload;
|
|
337
341
|
/**
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Download of a recording held on a HomeBase
|
|
2
|
+
* Download of a recording held on a HomeBase, over the station's P2P session. {@link RECORDING_STATIONS}
|
|
3
|
+
* holds every way the station models differ; the rest is shared.
|
|
3
4
|
*
|
|
4
5
|
* The request is `CMD_DOWNLOAD_VIDEO` (1024) in the string-pair shape
|
|
5
6
|
* ({@link buildStringPairCommandPayload}) on the camera's channel. The station answers on
|
|
6
7
|
* {@link RECORDING_DATA_TYPE}, tagged with that channel, with the recording's `CMD_VIDEO_FRAME` (1300) and
|
|
7
8
|
* `CMD_AUDIO_FRAME` (1301) frames, faster than real time, then `CMD_DOWNLOAD_FINISH` (1304) on the control
|
|
8
|
-
* data type, tagged with
|
|
9
|
+
* data type, tagged with the station's {@link RecordingStation.finishChannel} whatever the camera's channel.
|
|
9
10
|
*
|
|
10
|
-
* Frame layouts:
|
|
11
|
+
* Frame layouts on a station that seals its recordings (HomeBase 2):
|
|
11
12
|
*
|
|
12
13
|
* ```
|
|
13
14
|
* video, keyframe: the live E2E layout ({@link VideoFrameDecoder}), length = 179 + len exactly
|
|
@@ -18,10 +19,17 @@
|
|
|
18
19
|
* plaintext is a whole ADTS frame, header included
|
|
19
20
|
* ```
|
|
20
21
|
*
|
|
22
|
+
* Frame layouts on a station that stores them in the clear (HomeBase 3):
|
|
23
|
+
*
|
|
24
|
+
* ```
|
|
25
|
+
* video, every frame: the same 22B header, then len bytes of Annex-B H.264 or H.265, length = 22 + len
|
|
26
|
+
* audio: the same 16B header, then len bytes of ADTS AAC
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
21
29
|
* @module p2p/recording-download
|
|
22
30
|
*/
|
|
23
31
|
import { type RecordingDownload } from "../../core/contracts.js";
|
|
24
|
-
import type
|
|
32
|
+
import { type P2PSession } from "./p2p-session.js";
|
|
25
33
|
/** Data type a station sends a recording's frames on: the binary channel (`0xd1 0x03`), not live video's. */
|
|
26
34
|
export declare const RECORDING_DATA_TYPE = 3;
|
|
27
35
|
/**
|
|
@@ -36,12 +44,56 @@ export interface RecordingFrame {
|
|
|
36
44
|
raw: Buffer;
|
|
37
45
|
}
|
|
38
46
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
47
|
+
* How one station model stores its attached cameras' recordings and serves them over `CMD_DOWNLOAD_VIDEO`
|
|
48
|
+
* (1024): where a recording lives, whether it is sealed, which channel the finish frame is tagged with, and
|
|
49
|
+
* where a split picture continues. A model without an entry in {@link RECORDING_STATIONS} offers no download.
|
|
50
|
+
*/
|
|
51
|
+
export interface RecordingStation {
|
|
52
|
+
/**
|
|
53
|
+
* The path the station stores the recording named `recording` of the camera on station channel `channel`
|
|
54
|
+
* under. Answers `undefined` for a name the station could not hold. The SDK builds every path from these two
|
|
55
|
+
* values, so a request never reaches outside the camera's own folder.
|
|
56
|
+
*/
|
|
57
|
+
readonly path: (channel: number, recording: string) => string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the station stores a recording sealed under its event's cipher, which opens with that cipher's ECC
|
|
60
|
+
* private key, or in the clear, which reads no key. The frame layouts of each are in this module's
|
|
61
|
+
* documentation.
|
|
62
|
+
*/
|
|
63
|
+
readonly sealed: boolean;
|
|
64
|
+
/** The channel the station tags `CMD_DOWNLOAD_FINISH` (1304) with. The camera's own channel is taken too. */
|
|
65
|
+
readonly finishChannel: number;
|
|
66
|
+
/**
|
|
67
|
+
* The payload size the station fills a `CMD_VIDEO_FRAME` to when an access unit continues in the next frame
|
|
68
|
+
* ({@link AccessUnitAssembler}).
|
|
69
|
+
*/
|
|
70
|
+
readonly chunkBytes: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The path a HomeBase 2 stores a camera's recording under: its {@link cameraFolder}, then the recording's name
|
|
74
|
+
* as `yyyyMMddHHmmss` or `h265_yyyyMMddHHmmss`. Answers `undefined` for any other name, and for a channel
|
|
75
|
+
* {@link cameraFolder} refuses. Both forms are digits behind one fixed prefix, so the anchored test lets no
|
|
76
|
+
* `..`, slash or other character that could reach another path on the station through.
|
|
43
77
|
*/
|
|
44
78
|
export declare function homeBase2RecordingPath(channel: number, recording: string): string | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* The path a HomeBase 3 stores a camera's recording under:
|
|
81
|
+
* `/zx/emmcdata/<cameraFolder>/<yyyyMM>/<name>/<name>.zxvideo`, where the name is the recording's start as
|
|
82
|
+
* `yyyyMMddHHmmss`, whatever the codec, and `<yyyyMM>` is its first six digits. Answers `undefined` for any
|
|
83
|
+
* other name (a codec prefix, an extension or a path included), and for a channel {@link cameraFolder}
|
|
84
|
+
* refuses. The anchored test lets no character but digits through.
|
|
85
|
+
*/
|
|
86
|
+
export declare function homeBase3RecordingPath(channel: number, recording: string): string | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* The stations whose recording download is confirmed, by station model.
|
|
89
|
+
*
|
|
90
|
+
* `T8010`, HomeBase 2: `/media/mmcblk0p1/CameraNN/<name>.dat`, sealed under the event's cipher, finish tagged
|
|
91
|
+
* with channel 0, split pictures filled to {@link STATION_CHUNK_BYTES}.
|
|
92
|
+
*
|
|
93
|
+
* `T8030`, HomeBase 3: `/zx/emmcdata/CameraNN/<yyyyMM>/<name>/<name>.zxvideo`, in the clear, finish tagged
|
|
94
|
+
* with {@link STATION_CHANNEL}, a keyframe over 400000 bytes split at exactly 400000.
|
|
95
|
+
*/
|
|
96
|
+
export declare const RECORDING_STATIONS: ReadonlyMap<string, RecordingStation>;
|
|
45
97
|
/** One recording transfer on a station session. */
|
|
46
98
|
export interface RecordingTransfer {
|
|
47
99
|
/** The recording's frames, in arrival order. Rejects as {@link receiveRecording} describes. */
|
|
@@ -52,7 +104,8 @@ export interface RecordingTransfer {
|
|
|
52
104
|
/**
|
|
53
105
|
* Request one recording and collect its frames until `CMD_DOWNLOAD_FINISH`. Only frames on
|
|
54
106
|
* {@link RECORDING_DATA_TYPE} tagged with the camera's channel belong to it, so a live stream open on the same
|
|
55
|
-
* station is never mixed in. The finish frame is taken on the camera's channel or
|
|
107
|
+
* station is never mixed in. The finish frame is taken on the camera's channel or on `finishChannel`
|
|
108
|
+
* ({@link RecordingStation.finishChannel}).
|
|
56
109
|
*
|
|
57
110
|
* `frames` rejects with {@link RecordingDownloadError}: `no-data` when nothing arrives within
|
|
58
111
|
* {@link FIRST_FRAME_WAIT_MS}, and `incomplete` when the transfer ends without its finish frame, on a
|
|
@@ -64,14 +117,22 @@ export declare function receiveRecording(session: P2PSession, request: {
|
|
|
64
117
|
path: string;
|
|
65
118
|
accountId: string;
|
|
66
119
|
channel: number;
|
|
120
|
+
finishChannel: number;
|
|
67
121
|
timeoutMs?: number;
|
|
68
122
|
signal?: AbortSignal;
|
|
69
123
|
}): RecordingTransfer;
|
|
70
124
|
/**
|
|
71
|
-
* Decode a recording's frames, in arrival order, into elementary streams.
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
125
|
+
* Decode a recording's frames, in arrival order, into elementary streams. `station` sets where a split
|
|
126
|
+
* picture continues ({@link RecordingStation.chunkBytes}) and whether the frames are sealed
|
|
127
|
+
* ({@link RecordingStation.sealed}). On a sealed station a keyframe goes through one {@link VideoFrameDecoder}
|
|
128
|
+
* under `eccPrivateKeyHex`, and audio opens under that decoder's current media key; a keyframe that does not
|
|
129
|
+
* open, and audio before the first media key, are dropped. On a station in the clear every video payload is read
|
|
130
|
+
* as it is, and audio is everything after its 16-byte header; a keyframe that is in fact sealed begins with an
|
|
131
|
+
* ECIES point rather than a start code, so the assembler drops it. Split pictures are assembled before they
|
|
132
|
+
* enter the output, and an orphan continuation cannot become its own picture. Audio frames whose codec byte is
|
|
133
|
+
* not AAC-LC are dropped.
|
|
134
|
+
*
|
|
135
|
+
* Rejects with {@link RecordingDownloadError} `undecodable` when no video frame decodes, and on a sealed
|
|
136
|
+
* station whose key is absent or not a P-256 key.
|
|
76
137
|
*/
|
|
77
|
-
export declare function decodeRecording(frames: readonly RecordingFrame[],
|
|
138
|
+
export declare function decodeRecording(frames: readonly RecordingFrame[], station: Pick<RecordingStation, "sealed" | "chunkBytes">, eccPrivateKeyHex?: string): RecordingDownload;
|
|
@@ -122,23 +122,27 @@ export declare const STATION_CHUNK_BYTES = 64000;
|
|
|
122
122
|
*
|
|
123
123
|
* Two consequences worth stating. A unit that is exactly the threshold long, or an exact multiple of it,
|
|
124
124
|
* ends on a full frame and is dropped as truncated — one frame in ~64000, reported, and the alternative is
|
|
125
|
-
* handing a decoder bytes that may genuinely be short. And a station that fills to some OTHER size is
|
|
126
|
-
* recognised: its units stay exactly as they arrive
|
|
125
|
+
* handing a decoder bytes that may genuinely be short. And a station that fills to some OTHER size is
|
|
126
|
+
* recognised only by an assembler constructed with that size: elsewhere its units stay exactly as they arrive
|
|
127
|
+
* rather than being mis-joined.
|
|
127
128
|
*/
|
|
128
129
|
export declare class AccessUnitAssembler {
|
|
129
130
|
private readonly onDropped?;
|
|
131
|
+
private readonly chunkBytes;
|
|
130
132
|
private open?;
|
|
131
133
|
private droppedUnits;
|
|
132
134
|
/**
|
|
133
135
|
* @param onDropped called for each incomplete unit discarded, with what had arrived, how many frames it
|
|
134
136
|
* arrived in, and how many this assembler has dropped in total — a running count, so no caller keeps
|
|
135
137
|
* its own.
|
|
138
|
+
* @param chunkBytes the payload size the station fills a frame to before continuing a unit in the next. A
|
|
139
|
+
* unit is held while its latest frame declares exactly this. {@link STATION_CHUNK_BYTES} when omitted.
|
|
136
140
|
*/
|
|
137
141
|
constructor(onDropped?: ((drop: {
|
|
138
142
|
carried: number;
|
|
139
143
|
chunks: number;
|
|
140
144
|
count: number;
|
|
141
|
-
}) => void) | undefined);
|
|
145
|
+
}) => void) | undefined, chunkBytes?: number);
|
|
142
146
|
/**
|
|
143
147
|
* Feed one raw `CMD_VIDEO_FRAME` payload; returns the access units it completed — none while a unit is
|
|
144
148
|
* still being filled, one in the ordinary case.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mega-yfue/eufy-sdk",
|
|
3
|
-
"version": "0.6.0-beta.
|
|
3
|
+
"version": "0.6.0-beta.31",
|
|
4
4
|
"description": "One typed TypeScript client for the Anker eufy v6 cloud — capability-driven devices, realtime events over P2P/MQTT/push, and live media",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "mega-yfue",
|