@sentinel-nvr/web 0.1.0
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/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/api/index.d.ts +20 -0
- package/dist/api/index.js +261 -0
- package/dist/api/index.js.map +1 -0
- package/dist/client-BU0GTX3b.d.ts +233 -0
- package/dist/player/index.d.ts +255 -0
- package/dist/player/index.js +1867 -0
- package/dist/player/index.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { f as SentinelClip, e as SentinelClient } from '../client-BU0GTX3b.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PlayerController — the whole playback brain of the NVR. Framework-free: it owns
|
|
5
|
+
* a <video>, a freeze <canvas> and an MJPEG <img> inside a stage element, and
|
|
6
|
+
* reports state through `onState`; the host only renders what it says.
|
|
7
|
+
*
|
|
8
|
+
* Host seams: the API client is injected; status labels are i18n KEY suffixes
|
|
9
|
+
* (`nvr.player.<label>`) the host translates; `storagePrefix` namespaces the
|
|
10
|
+
* remembered stage aspect per camera; `brand` tags telemetry lines. The MSE paths
|
|
11
|
+
* are gated on `SentinelClient.corsMedia`; posters load with crossOrigin so the
|
|
12
|
+
* freeze canvas stays untainted. The host CSS must define a `.hidden` class.
|
|
13
|
+
*
|
|
14
|
+
* Paths: LIVE = WebRTC (sink) → MSE live (fMP4, no audio) → MJPEG <img>.
|
|
15
|
+
* RECORDED = WebRTC relay (server-seekable, scrub time-lapse in place) →
|
|
16
|
+
* MSE fallback (progressive segments, trick-play chase) → native <video src>.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** `label` is an i18n key suffix: nvr.player.<label> */
|
|
20
|
+
type PlayerLabel = 'live' | 'liveWebrtc' | 'liveMse' | 'liveMjpeg' | 'loading' | 'playing' | 'paused' | 'scrub' | 'noRecording' | '';
|
|
21
|
+
interface PlayerState {
|
|
22
|
+
live: boolean;
|
|
23
|
+
label: PlayerLabel;
|
|
24
|
+
playhead: number | null;
|
|
25
|
+
rate: number;
|
|
26
|
+
sound: boolean;
|
|
27
|
+
paused: boolean;
|
|
28
|
+
transport: 'webrtc' | 'mse' | 'mjpeg' | 'relay' | 'native' | 'none';
|
|
29
|
+
muted: boolean;
|
|
30
|
+
}
|
|
31
|
+
interface PlayerRefs {
|
|
32
|
+
video: HTMLVideoElement;
|
|
33
|
+
freeze: HTMLCanvasElement;
|
|
34
|
+
img: HTMLImageElement;
|
|
35
|
+
stage: HTMLElement;
|
|
36
|
+
}
|
|
37
|
+
interface PlayerOptions {
|
|
38
|
+
onState: (s: PlayerState) => void;
|
|
39
|
+
onClipsRefresh: () => Promise<SentinelClip[]>;
|
|
40
|
+
/** localStorage key prefix for the remembered stage aspect per camera (default 'snvr-ar-'). */
|
|
41
|
+
storagePrefix?: string;
|
|
42
|
+
/** telemetry brand tag written into api/clientlog lines (default 'web'). */
|
|
43
|
+
brand?: string;
|
|
44
|
+
}
|
|
45
|
+
type Opts = PlayerOptions;
|
|
46
|
+
declare class PlayerController {
|
|
47
|
+
private v;
|
|
48
|
+
private fz;
|
|
49
|
+
private img;
|
|
50
|
+
private stage;
|
|
51
|
+
private api;
|
|
52
|
+
private opts;
|
|
53
|
+
camId: string;
|
|
54
|
+
camName: string;
|
|
55
|
+
clips: SentinelClip[];
|
|
56
|
+
codecs: string | null;
|
|
57
|
+
rangeStart: number;
|
|
58
|
+
rangeEnd: number;
|
|
59
|
+
live: boolean;
|
|
60
|
+
rate: number;
|
|
61
|
+
soundOn: boolean;
|
|
62
|
+
recPaused: boolean;
|
|
63
|
+
recPausedTs: number | null;
|
|
64
|
+
recWebrtcDisabled: boolean;
|
|
65
|
+
label: PlayerLabel;
|
|
66
|
+
transport: PlayerState['transport'];
|
|
67
|
+
private rw;
|
|
68
|
+
private rwStartMs;
|
|
69
|
+
private rwBase;
|
|
70
|
+
private rwPosTs;
|
|
71
|
+
private rwPosAt;
|
|
72
|
+
private rwRate;
|
|
73
|
+
private rwSrate;
|
|
74
|
+
private rwScrub;
|
|
75
|
+
private rwSeekBusy;
|
|
76
|
+
private rwPending;
|
|
77
|
+
private rwLastSeekTs;
|
|
78
|
+
private rwLastSeekAt;
|
|
79
|
+
private rwRateBusy;
|
|
80
|
+
private rwPendingRate;
|
|
81
|
+
private rwLastRateAt;
|
|
82
|
+
private relayPoll;
|
|
83
|
+
private wdLastCt;
|
|
84
|
+
private wdLastAt;
|
|
85
|
+
private wdDead;
|
|
86
|
+
private wdGrace;
|
|
87
|
+
private wdResumeLogged;
|
|
88
|
+
private recoveries;
|
|
89
|
+
private posterUntilSeek;
|
|
90
|
+
private seekTarget;
|
|
91
|
+
private cmdSeq;
|
|
92
|
+
private seekAt;
|
|
93
|
+
private w;
|
|
94
|
+
private L;
|
|
95
|
+
private M;
|
|
96
|
+
private CH;
|
|
97
|
+
private curClipId;
|
|
98
|
+
private playIndex;
|
|
99
|
+
private lastLoad;
|
|
100
|
+
private pendingTs;
|
|
101
|
+
private pendingT;
|
|
102
|
+
private fc;
|
|
103
|
+
private freezeT;
|
|
104
|
+
private freezeTok;
|
|
105
|
+
private cad;
|
|
106
|
+
private stallTimer;
|
|
107
|
+
private destroyed;
|
|
108
|
+
private unlisten;
|
|
109
|
+
private arPrefix;
|
|
110
|
+
constructor(api: SentinelClient, opts: Opts);
|
|
111
|
+
attach(r: PlayerRefs): void;
|
|
112
|
+
destroy(): void;
|
|
113
|
+
setCamera(camId: string, name: string): void;
|
|
114
|
+
/** picture aspect → stage box (mobile layout uses it); posters and video both report it */
|
|
115
|
+
private setAspect;
|
|
116
|
+
/** clips of the loaded range (several days, sorted), codec string, and the range the timeline spans */
|
|
117
|
+
setClips(clips: SentinelClip[], codecs: string | null, rangeStart: number, rangeEnd: number): void;
|
|
118
|
+
private setLabel;
|
|
119
|
+
private emit;
|
|
120
|
+
clipIndexFor(ts: number): number;
|
|
121
|
+
private nearNow;
|
|
122
|
+
private clampRange;
|
|
123
|
+
/** Wall-clock position of the picture on screen (Scrypted's getRecordingStreamCurrentTime). */
|
|
124
|
+
currentTs(): number | null;
|
|
125
|
+
private fcArm;
|
|
126
|
+
presentedFrames(): number;
|
|
127
|
+
private freezeArm;
|
|
128
|
+
freezeShow(hold?: boolean): void;
|
|
129
|
+
freezeFromImage(img: HTMLImageElement, hold: boolean, untilSeek?: boolean, kind?: string): boolean;
|
|
130
|
+
freezeHide(): void;
|
|
131
|
+
private posterUp;
|
|
132
|
+
freezeHold(): void;
|
|
133
|
+
/** Event click: the stored frame is the poster until the seek lands. */
|
|
134
|
+
posterEvent(ts: number): void;
|
|
135
|
+
/** Camera opened: newest snapshot in front of the black stage until live plays. */
|
|
136
|
+
posterFromSnapshot(): void;
|
|
137
|
+
private posterShow;
|
|
138
|
+
private safePlay;
|
|
139
|
+
goLive(): void;
|
|
140
|
+
private exitLiveState;
|
|
141
|
+
private setMjpeg;
|
|
142
|
+
private webrtcTeardown;
|
|
143
|
+
private liveStartWebrtc;
|
|
144
|
+
private liveFallbackMse;
|
|
145
|
+
private liveCodec;
|
|
146
|
+
private liveMseOk;
|
|
147
|
+
private liveTeardown;
|
|
148
|
+
private liveAppend;
|
|
149
|
+
private liveTrim;
|
|
150
|
+
private liveEdge;
|
|
151
|
+
private liveRestart;
|
|
152
|
+
private liveStartMse;
|
|
153
|
+
private liveFallbackImg;
|
|
154
|
+
private recWebrtcOk;
|
|
155
|
+
private recWebrtcTeardown;
|
|
156
|
+
private recWebrtcStart;
|
|
157
|
+
private recFallbackMse;
|
|
158
|
+
/** Hard server-side seek, coalesced: one in flight, latest wins, 150 ms floor. */
|
|
159
|
+
private recRelaySeek;
|
|
160
|
+
/** Scrub time-lapse rate, coalesced (120 ms floor, 15 % hysteresis). */
|
|
161
|
+
private recRelayRate;
|
|
162
|
+
private recRelayScrub;
|
|
163
|
+
private relayRecover;
|
|
164
|
+
private startRelayPoll;
|
|
165
|
+
private stopRelayPoll;
|
|
166
|
+
private cadStart;
|
|
167
|
+
private cadStop;
|
|
168
|
+
playAt(ts: number, opts?: {
|
|
169
|
+
scrub?: boolean;
|
|
170
|
+
srate?: number;
|
|
171
|
+
noChase?: boolean;
|
|
172
|
+
}): void;
|
|
173
|
+
private mseSupported;
|
|
174
|
+
private mseTeardown;
|
|
175
|
+
private bufferedContains;
|
|
176
|
+
private mseStart;
|
|
177
|
+
private drain;
|
|
178
|
+
private feed;
|
|
179
|
+
private handleStall;
|
|
180
|
+
private chaseAbort;
|
|
181
|
+
private endChase;
|
|
182
|
+
private startChase;
|
|
183
|
+
togglePlayPause(): void;
|
|
184
|
+
skip(ms: number): void;
|
|
185
|
+
cycleSpeed(): number;
|
|
186
|
+
setSound(on: boolean): void;
|
|
187
|
+
snapshot(): void;
|
|
188
|
+
private capLog;
|
|
189
|
+
pip(): void;
|
|
190
|
+
fullscreen(): void;
|
|
191
|
+
/** first gesture: scrub profile on (relay); MSE: freeze + pause */
|
|
192
|
+
scrubBegin(): void;
|
|
193
|
+
/** scroll position + velocity (timeline-ms per wall-s) while the gesture runs */
|
|
194
|
+
scrubMove(_centerTs: number, vel: number, smoothTs: number): void;
|
|
195
|
+
/** hold / release: land exactly on the centre at 1× (or go live near now) */
|
|
196
|
+
scrubSeek(ts: number, srate?: number): void;
|
|
197
|
+
/** gesture settled → normal profile, base rate */
|
|
198
|
+
scrubIdle(): void;
|
|
199
|
+
private hiddenTs;
|
|
200
|
+
private onVisibility;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* WebRTC session over the plugin's WebSocket signaling (docs/API.md §6).
|
|
205
|
+
* Battle-tested flow: options first, createLocalDescription/setRemoteDescription
|
|
206
|
+
* RPCs, trickle ICE, stream composed from ALL receivers (Safari attached an
|
|
207
|
+
* audio-only stream forever when the audio track arrived first), fresh offer on
|
|
208
|
+
* renegotiation, dec-check after connect with a one-time TURN-relay retry on a
|
|
209
|
+
* media blackhole. The API client is injected (hosts may run on another origin).
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
interface SessionCallbacks {
|
|
213
|
+
onStream: (ms: MediaStream) => void;
|
|
214
|
+
onFail: (reason: string) => void;
|
|
215
|
+
onSessionId?: (id: string) => void;
|
|
216
|
+
}
|
|
217
|
+
interface SessionOptions {
|
|
218
|
+
camId: string;
|
|
219
|
+
mode: 'live' | 'recorded';
|
|
220
|
+
startMs?: number;
|
|
221
|
+
compat?: boolean;
|
|
222
|
+
forceRelay?: boolean;
|
|
223
|
+
kind?: string;
|
|
224
|
+
}
|
|
225
|
+
declare function mobileClient(): boolean;
|
|
226
|
+
declare class WebRtcSession {
|
|
227
|
+
ws: WebSocket | undefined;
|
|
228
|
+
pc: RTCPeerConnection | undefined;
|
|
229
|
+
id: string | undefined;
|
|
230
|
+
active: boolean;
|
|
231
|
+
private connectT;
|
|
232
|
+
private trackSig;
|
|
233
|
+
private api;
|
|
234
|
+
private opts;
|
|
235
|
+
private cb;
|
|
236
|
+
constructor(api: SentinelClient, opts: SessionOptions, cb: SessionCallbacks);
|
|
237
|
+
get connected(): boolean;
|
|
238
|
+
start(): void;
|
|
239
|
+
stop(): void;
|
|
240
|
+
private fail;
|
|
241
|
+
private ensurePc;
|
|
242
|
+
private createLocal;
|
|
243
|
+
private setRemote;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Client telemetry → the plugin's `api/clientlog` → its server log as `[client]`
|
|
248
|
+
* lines (phones have no console; this is how playback problems on a device get
|
|
249
|
+
* diagnosed). `brand` tags which host emitted the line (e.g. 'sentinel', 'hapulse').
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
declare function setRlogClient(c: SentinelClient | null, b?: string): void;
|
|
253
|
+
declare function rlog(tag: string, data?: Record<string, unknown>): void;
|
|
254
|
+
|
|
255
|
+
export { PlayerController, type PlayerLabel, type PlayerOptions, type PlayerRefs, type PlayerState, type SessionCallbacks, type SessionOptions, WebRtcSession, mobileClient, rlog, setRlogClient };
|