@push.rocks/smartbrowser 4.2.0 → 4.3.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.
@@ -0,0 +1,46 @@
1
+ import type { ILiveBrowserState, ILiveVideoSource } from '@push.rocks/smartpuppeteer';
2
+ import type { ILiveBrowserCanvasError } from './interfaces.livebrowsercanvas.js';
3
+ import type { ILiveBrowserVideoClient, ILiveBrowserVideoStatistics } from './interfaces.livebrowservideo.js';
4
+ interface IVideoReceiverOptions {
5
+ video: HTMLVideoElement;
6
+ client: ILiveBrowserVideoClient;
7
+ operationTimeoutMs: number;
8
+ onUnavailable(): void;
9
+ onPresented(sourceArg: ILiveVideoSource): void;
10
+ onError(errorArg: ILiveBrowserCanvasError): void;
11
+ }
12
+ /** One native receiver. Every replacement closes the previous host peer before opening another. */
13
+ export declare class LiveBrowserVideoReceiver {
14
+ private readonly options;
15
+ private epoch;
16
+ private desiredKey?;
17
+ private controller?;
18
+ private peer?;
19
+ private frameCallback?;
20
+ private connectionTimer?;
21
+ private sampleTimer?;
22
+ private tail;
23
+ private remoteMayExist;
24
+ private statistics?;
25
+ private decodedBeforePeer;
26
+ private droppedBeforePeer;
27
+ constructor(options: IVideoReceiverOptions);
28
+ getStatistics(): ILiveBrowserVideoStatistics | undefined;
29
+ getCounters(): {
30
+ framesReceived: number;
31
+ framesDecoded: number;
32
+ framesSkipped: number;
33
+ };
34
+ update(stateArg: ILiveBrowserState): void;
35
+ /** Synchronously prevents old frames or input from surviving a transport/source change. */
36
+ interrupt(): void;
37
+ stop(): Promise<void>;
38
+ private enqueue;
39
+ private closeRemote;
40
+ private connect;
41
+ private waitForIce;
42
+ private fail;
43
+ private sample;
44
+ private operation;
45
+ }
46
+ export {};
@@ -0,0 +1,243 @@
1
+ const sourceKey = (sourceArg) => JSON.stringify([
2
+ sourceArg.tabId, sourceArg.generation, sourceArg.viewportRevision,
3
+ sourceArg.viewport.width, sourceArg.viewport.height, sourceArg.viewport.deviceScaleFactor,
4
+ ]);
5
+ /** One native receiver. Every replacement closes the previous host peer before opening another. */
6
+ export class LiveBrowserVideoReceiver {
7
+ constructor(options) {
8
+ this.options = options;
9
+ this.epoch = 0;
10
+ this.tail = Promise.resolve();
11
+ this.remoteMayExist = false;
12
+ this.decodedBeforePeer = 0;
13
+ this.droppedBeforePeer = 0;
14
+ const video = options.video;
15
+ video.autoplay = true;
16
+ video.muted = true;
17
+ video.playsInline = true;
18
+ video.style.objectFit = 'contain';
19
+ }
20
+ getStatistics() {
21
+ return this.statistics ? { ...this.statistics } : undefined;
22
+ }
23
+ getCounters() {
24
+ const decoded = this.decodedBeforePeer + (this.statistics?.framesDecoded ?? 0);
25
+ const dropped = this.droppedBeforePeer + (this.statistics?.framesDropped ?? 0);
26
+ return { framesReceived: decoded + dropped, framesDecoded: decoded, framesSkipped: dropped };
27
+ }
28
+ update(stateArg) {
29
+ const tab = stateArg.tabs.find((tabArg) => tabArg.id === stateArg.activeTabId);
30
+ const source = document.visibilityState !== 'hidden'
31
+ && stateArg.status === 'running' && tab?.status === 'open' && tab.streaming
32
+ ? { tabId: tab.id, generation: tab.generation, viewportRevision: stateArg.viewportRevision,
33
+ viewport: { ...stateArg.viewport } }
34
+ : undefined;
35
+ const key = source ? sourceKey(source) : undefined;
36
+ if (key === this.desiredKey)
37
+ return;
38
+ this.interrupt();
39
+ this.desiredKey = key;
40
+ const epoch = this.epoch;
41
+ const controller = new AbortController();
42
+ this.controller = controller;
43
+ this.enqueue(async () => {
44
+ await this.closeRemote();
45
+ if (!source || epoch !== this.epoch)
46
+ return;
47
+ try {
48
+ await this.connect(source, controller.signal, epoch);
49
+ }
50
+ catch (error) {
51
+ if (epoch !== this.epoch || controller.signal.aborted)
52
+ return;
53
+ this.fail('video_negotiation_failed', error, epoch);
54
+ }
55
+ });
56
+ }
57
+ /** Synchronously prevents old frames or input from surviving a transport/source change. */
58
+ interrupt() {
59
+ this.epoch++;
60
+ this.desiredKey = undefined;
61
+ this.controller?.abort(new Error('Video source was superseded'));
62
+ this.controller = undefined;
63
+ this.options.onUnavailable();
64
+ if (this.frameCallback !== undefined)
65
+ this.options.video.cancelVideoFrameCallback(this.frameCallback);
66
+ this.frameCallback = undefined;
67
+ clearTimeout(this.connectionTimer);
68
+ clearTimeout(this.sampleTimer);
69
+ this.connectionTimer = undefined;
70
+ this.sampleTimer = undefined;
71
+ this.peer?.close();
72
+ this.peer = undefined;
73
+ this.options.video.pause();
74
+ this.options.video.srcObject = null;
75
+ this.decodedBeforePeer += this.statistics?.framesDecoded ?? 0;
76
+ this.droppedBeforePeer += this.statistics?.framesDropped ?? 0;
77
+ this.statistics = undefined;
78
+ }
79
+ async stop() {
80
+ this.interrupt();
81
+ await this.enqueue(() => this.closeRemote());
82
+ }
83
+ enqueue(operationArg) {
84
+ const operation = this.tail.then(operationArg, operationArg);
85
+ this.tail = operation.catch((error) => {
86
+ this.options.onError({ code: 'renderer_cleanup_failed', message: `Video peer cleanup failed: ${String(error)}`, cause: error });
87
+ });
88
+ return operation;
89
+ }
90
+ async closeRemote() {
91
+ if (!this.remoteMayExist)
92
+ return;
93
+ await this.operation((optionsArg) => this.options.client.closeVideoPeer(optionsArg));
94
+ this.remoteMayExist = false;
95
+ }
96
+ async connect(sourceArg, signalArg, epochArg) {
97
+ this.remoteMayExist = true;
98
+ const offer = await this.operation((optionsArg) => this.options.client.openVideoPeer(optionsArg), signalArg);
99
+ if (sourceKey(offer.source) !== sourceKey(sourceArg))
100
+ throw new Error('Video offer does not match the current browser source');
101
+ if (signalArg.aborted || epochArg !== this.epoch)
102
+ return;
103
+ const peer = new RTCPeerConnection({ iceServers: offer.iceServers, iceTransportPolicy: offer.iceTransportPolicy });
104
+ this.peer = peer;
105
+ this.statistics = { connectionState: 'new', framesDecoded: 0, framesDropped: 0, bytesReceived: 0, bitrate: 0 };
106
+ peer.addEventListener('connectionstatechange', () => {
107
+ if (epochArg !== this.epoch)
108
+ return;
109
+ if (this.statistics)
110
+ this.statistics.connectionState = peer.connectionState;
111
+ if (peer.connectionState === 'failed' || peer.connectionState === 'disconnected') {
112
+ this.fail('video_connection_failed', new Error(`Video connection ${peer.connectionState}`), epochArg);
113
+ }
114
+ });
115
+ let presented = false;
116
+ peer.addEventListener('track', (eventArg) => {
117
+ if (epochArg !== this.epoch || eventArg.track.kind !== 'video')
118
+ return;
119
+ if ('jitterBufferTarget' in eventArg.receiver)
120
+ eventArg.receiver.jitterBufferTarget = 0;
121
+ this.options.video.srcObject = new MediaStream([eventArg.track]);
122
+ this.frameCallback = this.options.video.requestVideoFrameCallback(() => {
123
+ this.frameCallback = undefined;
124
+ if (epochArg !== this.epoch)
125
+ return;
126
+ clearTimeout(this.connectionTimer);
127
+ this.connectionTimer = undefined;
128
+ presented = true;
129
+ this.options.onPresented(sourceArg);
130
+ });
131
+ void this.options.video.play().catch((error) => this.fail('video_connection_failed', error, epochArg));
132
+ });
133
+ await this.operation(async ({ signal }) => {
134
+ await peer.setRemoteDescription(offer.description);
135
+ if (signal.aborted)
136
+ throw signal.reason;
137
+ await peer.setLocalDescription(await peer.createAnswer());
138
+ await this.waitForIce(peer, signal);
139
+ }, signalArg);
140
+ if (signalArg.aborted || epochArg !== this.epoch)
141
+ return;
142
+ const description = peer.localDescription;
143
+ if (!description?.sdp || description.type !== 'answer' || description.sdp.length > 48 * 1024) {
144
+ throw new Error('Invalid video answer');
145
+ }
146
+ await this.operation((optionsArg) => this.options.client.answerVideoPeer(offer.negotiationId, { type: 'answer', sdp: description.sdp }, optionsArg), signalArg);
147
+ if (signalArg.aborted || epochArg !== this.epoch)
148
+ return;
149
+ if (!presented)
150
+ this.connectionTimer = setTimeout(() => this.fail('video_connection_failed', new Error('No video frame received within 30 seconds'), epochArg), 30000);
151
+ void this.sample(peer, signalArg, epochArg);
152
+ }
153
+ waitForIce(peerArg, signalArg) {
154
+ return new Promise((resolve, reject) => {
155
+ const cleanup = () => {
156
+ peerArg.removeEventListener('icegatheringstatechange', check);
157
+ signalArg.removeEventListener('abort', abort);
158
+ };
159
+ const abort = () => { cleanup(); reject(signalArg.reason); };
160
+ const check = () => {
161
+ if (signalArg.aborted)
162
+ abort();
163
+ else if (peerArg.iceGatheringState === 'complete') {
164
+ cleanup();
165
+ resolve();
166
+ }
167
+ };
168
+ peerArg.addEventListener('icegatheringstatechange', check);
169
+ signalArg.addEventListener('abort', abort, { once: true });
170
+ check();
171
+ });
172
+ }
173
+ fail(codeArg, errorArg, epochArg) {
174
+ if (epochArg !== this.epoch)
175
+ return;
176
+ this.interrupt();
177
+ void this.enqueue(() => this.closeRemote()).catch(() => undefined);
178
+ this.options.onError({ code: codeArg, message: String(errorArg), cause: errorArg });
179
+ }
180
+ async sample(peerArg, signalArg, epochArg, previousArg) {
181
+ let previous = previousArg;
182
+ try {
183
+ const [report, sender] = await this.operation(async (optionsArg) => Promise.all([
184
+ peerArg.getStats(), this.options.client.getVideoStatistics(optionsArg),
185
+ ]), signalArg);
186
+ if (epochArg !== this.epoch || !this.statistics)
187
+ return;
188
+ this.statistics.sender = sender;
189
+ report.forEach((entryArg) => {
190
+ if (entryArg.type === 'inbound-rtp' && entryArg.kind === 'video') {
191
+ const interval = previous ? entryArg.timestamp - previous.timestamp : 0;
192
+ this.statistics.framesDecoded = entryArg.framesDecoded ?? 0;
193
+ this.statistics.framesDropped = entryArg.framesDropped ?? 0;
194
+ this.statistics.bytesReceived = entryArg.bytesReceived ?? 0;
195
+ this.statistics.framesPerSecond = entryArg.framesPerSecond;
196
+ this.statistics.decoderImplementation = entryArg.decoderImplementation;
197
+ this.statistics.powerEfficientDecoder = entryArg.powerEfficientDecoder;
198
+ this.statistics.codec = report.get(entryArg.codecId)?.mimeType;
199
+ this.statistics.bitrate = interval > 0 ? Math.max(0, (entryArg.bytesReceived - previous.bytes) * 8000 / interval) : 0;
200
+ const emitted = entryArg.jitterBufferEmittedCount ?? 0;
201
+ const delay = entryArg.jitterBufferDelay ?? 0;
202
+ if (previous && emitted > previous.emitted)
203
+ this.statistics.jitterBufferMs =
204
+ (delay - previous.delay) * 1000 / (emitted - previous.emitted);
205
+ previous = { timestamp: entryArg.timestamp, bytes: entryArg.bytesReceived ?? 0, delay, emitted };
206
+ }
207
+ if (entryArg.type === 'transport' && entryArg.selectedCandidatePairId) {
208
+ const pair = report.get(entryArg.selectedCandidatePairId);
209
+ this.statistics.roundTripMs = pair?.currentRoundTripTime === undefined ? undefined : pair.currentRoundTripTime * 1000;
210
+ this.statistics.candidateType = report.get(pair?.remoteCandidateId)?.candidateType;
211
+ }
212
+ });
213
+ }
214
+ catch (error) {
215
+ if (epochArg !== this.epoch || signalArg.aborted)
216
+ return;
217
+ this.options.onError({ code: 'video_statistics_failed', message: `Could not read video statistics: ${String(error)}`, cause: error });
218
+ }
219
+ if (epochArg === this.epoch && !signalArg.aborted) {
220
+ this.sampleTimer = setTimeout(() => { void this.sample(peerArg, signalArg, epochArg, previous); }, 1000);
221
+ }
222
+ }
223
+ operation(operationArg, parentSignalArg) {
224
+ const controller = new AbortController();
225
+ const abort = () => controller.abort(parentSignalArg?.reason);
226
+ parentSignalArg?.addEventListener('abort', abort, { once: true });
227
+ if (parentSignalArg?.aborted)
228
+ abort();
229
+ const timer = setTimeout(() => controller.abort(new Error('Video operation timed out')), this.options.operationTimeoutMs);
230
+ return new Promise((resolve, reject) => {
231
+ const onAbort = () => reject(controller.signal.reason);
232
+ controller.signal.addEventListener('abort', onAbort, { once: true });
233
+ if (controller.signal.aborted)
234
+ onAbort();
235
+ else
236
+ Promise.resolve().then(() => operationArg({ signal: controller.signal })).then(resolve, reject);
237
+ }).finally(() => {
238
+ clearTimeout(timer);
239
+ parentSignalArg?.removeEventListener('abort', abort);
240
+ });
241
+ }
242
+ }
243
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5saXZlYnJvd3NlcnZpZGVvcmVjZWl2ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c193ZWIvY2xhc3Nlcy5saXZlYnJvd3NlcnZpZGVvcmVjZWl2ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBYUEsTUFBTSxTQUFTLEdBQUcsQ0FBQyxTQUEyQixFQUFVLEVBQUUsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDO0lBQ3hFLFNBQVMsQ0FBQyxLQUFLLEVBQUUsU0FBUyxDQUFDLFVBQVUsRUFBRSxTQUFTLENBQUMsZ0JBQWdCO0lBQ2pFLFNBQVMsQ0FBQyxRQUFRLENBQUMsS0FBSyxFQUFFLFNBQVMsQ0FBQyxRQUFRLENBQUMsTUFBTSxFQUFFLFNBQVMsQ0FBQyxRQUFRLENBQUMsaUJBQWlCO0NBQzFGLENBQUMsQ0FBQztBQUVILG1HQUFtRztBQUNuRyxNQUFNLE9BQU8sd0JBQXdCO0lBY25DLFlBQTZCLE9BQThCO1FBQTlCLFlBQU8sR0FBUCxPQUFPLENBQXVCO1FBYm5ELFVBQUssR0FBRyxDQUFDLENBQUM7UUFPVixTQUFJLEdBQWtCLE9BQU8sQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUN4QyxtQkFBYyxHQUFHLEtBQUssQ0FBQztRQUV2QixzQkFBaUIsR0FBRyxDQUFDLENBQUM7UUFDdEIsc0JBQWlCLEdBQUcsQ0FBQyxDQUFDO1FBRzVCLE1BQU0sS0FBSyxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUM7UUFDNUIsS0FBSyxDQUFDLFFBQVEsR0FBRyxJQUFJLENBQUM7UUFDdEIsS0FBSyxDQUFDLEtBQUssR0FBRyxJQUFJLENBQUM7UUFDbkIsS0FBSyxDQUFDLFdBQVcsR0FBRyxJQUFJLENBQUM7UUFDekIsS0FBSyxDQUFDLEtBQUssQ0FBQyxTQUFTLEdBQUcsU0FBUyxDQUFDO0lBQ3BDLENBQUM7SUFFTSxhQUFhO1FBQ2xCLE9BQU8sSUFBSSxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsRUFBRSxHQUFHLElBQUksQ0FB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
@@ -0,0 +1,6 @@
1
+ import { LiveBrowserRenderer } from './classes.livebrowserrenderer.js';
2
+ import type { ILiveBrowserVideoRendererOptions } from './interfaces.livebrowservideo.js';
3
+ /** Native video presentation; shares input, viewport and lifecycle handling with the canvas renderer. */
4
+ export declare class LiveBrowserVideoRenderer extends LiveBrowserRenderer {
5
+ constructor(optionsArg: ILiveBrowserVideoRendererOptions);
6
+ }
@@ -0,0 +1,8 @@
1
+ import { LiveBrowserRenderer } from './classes.livebrowserrenderer.js';
2
+ /** Native video presentation; shares input, viewport and lifecycle handling with the canvas renderer. */
3
+ export class LiveBrowserVideoRenderer extends LiveBrowserRenderer {
4
+ constructor(optionsArg) {
5
+ super(optionsArg);
6
+ }
7
+ }
8
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5saXZlYnJvd3NlcnZpZGVvcmVuZGVyZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c193ZWIvY2xhc3Nlcy5saXZlYnJvd3NlcnZpZGVvcmVuZGVyZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFHdkUseUdBQXlHO0FBQ3pHLE1BQU0sT0FBTyx3QkFBeUIsU0FBUSxtQkFBbUI7SUFDL0QsWUFBWSxVQUE0QztRQUN0RCxLQUFLLENBQUMsVUFBVSxDQUFDLENBQUM7SUFDcEIsQ0FBQztDQUNGIn0=
@@ -1,3 +1,6 @@
1
1
  export { LiveBrowserCanvasRenderer } from './classes.livebrowsercanvasrenderer.js';
2
2
  export type * from './interfaces.livebrowsercanvas.js';
3
3
  export type { ILiveBrowserError, ILiveBrowserErrorEvent, ILiveBrowserFrame, ILiveBrowserFrameAcknowledgement, ILiveBrowserFrameAcknowledgementRequest, ILiveBrowserFrameEvent, ILiveBrowserInputBase, ILiveBrowserInsertTextInput, ILiveBrowserKeyInput, ILiveBrowserModifierState, ILiveBrowserMouseInput, ILiveBrowserScreencastMetadata, ILiveBrowserState, ILiveBrowserStateEvent, ILiveBrowserTabState, ILiveBrowserViewport, ILiveBrowserWheelInput, TLiveBrowserEvent, TLiveBrowserEventListener, TLiveBrowserImageFormat, TLiveBrowserStatus, TLiveBrowserTabStatus, } from '@push.rocks/smartpuppeteer';
4
+ export { LiveBrowserVideoRenderer } from './classes.livebrowservideorenderer.js';
5
+ export type * from './interfaces.livebrowservideo.js';
6
+ export type { ILiveVideoDescription, ILiveVideoOffer, ILiveVideoSource, ILiveVideoStatistics, ILiveVideoAcceleration } from '@push.rocks/smartpuppeteer';
@@ -1,2 +1,3 @@
1
1
  export { LiveBrowserCanvasRenderer } from './classes.livebrowsercanvasrenderer.js';
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c193ZWIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLHlCQUF5QixFQUFFLE1BQU0sd0NBQXdDLENBQUMifQ==
2
+ export { LiveBrowserVideoRenderer } from './classes.livebrowservideorenderer.js';
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c193ZWIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLHlCQUF5QixFQUFFLE1BQU0sd0NBQXdDLENBQUM7QUE0Qm5GLE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLHVDQUF1QyxDQUFDIn0=
@@ -27,7 +27,7 @@ export interface ILiveBrowserCanvasOperationOptions {
27
27
  /** Adapters must propagate this signal and reject promptly when it aborts. */
28
28
  signal: AbortSignal;
29
29
  }
30
- export type TLiveBrowserCanvasErrorCode = 'frame_acknowledgement_failed' | 'frame_render_failed' | 'input_dispatch_failed' | 'input_queue_capacity_exceeded' | 'remote_browser_error' | 'renderer_callback_failed' | 'renderer_cleanup_failed' | 'viewport_update_failed';
30
+ export type TLiveBrowserCanvasErrorCode = 'video_negotiation_failed' | 'video_connection_failed' | 'video_statistics_failed' | 'frame_acknowledgement_failed' | 'frame_render_failed' | 'input_dispatch_failed' | 'input_queue_capacity_exceeded' | 'remote_browser_error' | 'renderer_callback_failed' | 'renderer_cleanup_failed' | 'viewport_update_failed';
31
31
  export interface ILiveBrowserCanvasError {
32
32
  code: TLiveBrowserCanvasErrorCode;
33
33
  message: string;
@@ -0,0 +1,31 @@
1
+ import type { ILiveVideoDescription, ILiveVideoOffer, ILiveVideoStatistics } from '@push.rocks/smartpuppeteer';
2
+ import type { ILiveBrowserCanvasClient, ILiveBrowserCanvasOperationOptions, ILiveBrowserCanvasRendererOptions, ILiveBrowserCanvasRendererStatistics } from './interfaces.livebrowsercanvas.js';
3
+ /** Signaling is authorized by the host's existing viewer lease. */
4
+ export interface ILiveBrowserVideoClient extends Omit<ILiveBrowserCanvasClient, 'acknowledgeFrame'> {
5
+ openVideoPeer(optionsArg: ILiveBrowserCanvasOperationOptions): Promise<Omit<ILiveVideoOffer, 'peerId'>>;
6
+ answerVideoPeer(negotiationIdArg: string, descriptionArg: ILiveVideoDescription, optionsArg: ILiveBrowserCanvasOperationOptions): Promise<void>;
7
+ closeVideoPeer(optionsArg: ILiveBrowserCanvasOperationOptions): Promise<void>;
8
+ getVideoStatistics(optionsArg: ILiveBrowserCanvasOperationOptions): Promise<ILiveVideoStatistics>;
9
+ }
10
+ export interface ILiveBrowserVideoRendererOptions extends Omit<ILiveBrowserCanvasRendererOptions, 'canvas' | 'client' | 'onFrameRendered' | 'frameDecodeTimeoutMs'> {
11
+ video: HTMLVideoElement;
12
+ client: ILiveBrowserVideoClient;
13
+ }
14
+ export interface ILiveBrowserVideoStatistics {
15
+ connectionState: RTCPeerConnectionState;
16
+ framesDecoded: number;
17
+ framesDropped: number;
18
+ bytesReceived: number;
19
+ bitrate: number;
20
+ framesPerSecond?: number;
21
+ roundTripMs?: number;
22
+ jitterBufferMs?: number;
23
+ codec?: string;
24
+ decoderImplementation?: string;
25
+ powerEfficientDecoder?: boolean;
26
+ candidateType?: string;
27
+ sender?: ILiveVideoStatistics;
28
+ }
29
+ export interface ILiveBrowserVideoRendererStatistics extends ILiveBrowserCanvasRendererStatistics {
30
+ video?: ILiveBrowserVideoStatistics;
31
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlcy5saXZlYnJvd3NlcnZpZGVvLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHNfd2ViL2ludGVyZmFjZXMubGl2ZWJyb3dzZXJ2aWRlby50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartbrowser",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "A simplified Puppeteer wrapper for easy automation and testing tasks.",
5
5
  "main": "dist_ts/index.js",
6
6
  "typings": "dist_ts/index.d.ts",
@@ -31,13 +31,13 @@
31
31
  "dependencies": {
32
32
  "@push.rocks/smartdelay": "^3.1.0",
33
33
  "@push.rocks/smartpdf": "^5.0.1",
34
- "@push.rocks/smartpuppeteer": "^2.7.1",
34
+ "@push.rocks/smartpuppeteer": "^2.8.1",
35
35
  "@push.rocks/smartunique": "^3.0.10"
36
36
  },
37
37
  "devDependencies": {
38
- "@git.zone/tsbuild": "^4.4.2",
38
+ "@git.zone/tsbuild": "^4.4.3",
39
39
  "@git.zone/tsrun": "^2.0.6",
40
- "@git.zone/tstest": "^4.0.0",
40
+ "@git.zone/tstest": "^6.0.0",
41
41
  "@types/node": "^26.4.1"
42
42
  },
43
43
  "private": false,
package/readme.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @push.rocks/smartbrowser
2
2
  A simplified Puppeteer wrapper for easy browser automation, PDF generation, screenshots, and page evaluation.
3
3
 
4
- The package also provides an isolated browser-side entry at `@push.rocks/smartbrowser/web`. Its `LiveBrowserCanvasRenderer` displays SmartPuppeteer live-session frames in a canvas and maps local input back to transport-neutral browser commands without bundling Puppeteer or Node.js code into the web application.
4
+ The package also provides an isolated browser-side entry at `@push.rocks/smartbrowser/web`. Its `LiveBrowserVideoRenderer` presents native WebRTC video, while `LiveBrowserCanvasRenderer` displays SmartPuppeteer image frames in a canvas and maps local input back to transport-neutral browser commands without bundling Puppeteer or Node.js code into the web application.
5
5
 
6
6
  ## Issue Reporting and Security
7
7
 
@@ -102,6 +102,28 @@ import { smartpuppeteer } from '@push.rocks/smartbrowser';
102
102
  const browser = await smartpuppeteer.getEnvAwareBrowserInstance();
103
103
  ```
104
104
 
105
+ ## Live Browser Video
106
+
107
+ `LiveBrowserVideoRenderer` presents a SmartPuppeteer video stream directly in an `HTMLVideoElement`. It shares the canvas renderer's bounded input queue, held-input cleanup, viewport negotiation, and suspend/resume lifecycle. There is no JavaScript image decoding or canvas copy in the video path.
108
+
109
+ ```typescript
110
+ import { LiveBrowserVideoRenderer, type ILiveBrowserVideoClient } from '@push.rocks/smartbrowser/web';
111
+
112
+ export async function mountVideo(video: HTMLVideoElement, client: ILiveBrowserVideoClient) {
113
+ const renderer = new LiveBrowserVideoRenderer({ video, client, resizeTarget: video.parentElement! });
114
+ await renderer.start();
115
+ return renderer;
116
+ }
117
+ ```
118
+
119
+ The client provides the same state and input operations as `ILiveBrowserCanvasClient`, replacing frame acknowledgements with `openVideoPeer(options)`, `answerVideoPeer(negotiationId, description, options)`, `closeVideoPeer(options)`, and `getVideoStatistics(options)`. Every asynchronous operation receives an abort signal and must settle promptly when cancelled. The host authenticates and owns the peer through the viewer's lease; callers do not choose peer IDs or ICE credentials. Subscribe to state/error events without JPEG frames. Keep the signaling transport alive until `stop()` completes, and close the host lease even when transport cleanup fails.
120
+
121
+ `start()` installs listeners and begins negotiation without waiting for video, allowing applications to activate their signaling transport afterward. Input remains blocked until the browser presents a frame for the exact active tab, stream generation and viewport revision. Source changes clear the old picture immediately. `suspend()`/`resume()` close and negotiate a fresh peer. Hiding the document releases its peer; showing it opens a new one. Native video uses muted autoplay, inline playback, and `object-fit: contain`; pointer coordinates exclude the surrounding letterbox.
122
+
123
+ ICE configuration comes from the host offer. With the default empty ICE server list, media connects directly over a reachable LAN or VPN without a relay or external STUN service. Signaling continues to use the application's authenticated transport. The receiver requests the lowest supported jitter-buffer target; the browser retains its network-dependent minimum. Chromium owns decoding, congestion control and frame scheduling.
124
+
125
+ `getStatistics()` includes the existing cumulative input/frame counters and an optional `video` snapshot: connection state, received bytes, bitrate, decoded/dropped frames, FPS, round-trip time, jitter-buffer delay, codec, decoder, selected candidate type and host encoder statistics when available. Sampling is sequential at one-second intervals. Hardware acceleration is reported only when the browser supplies it. Media negotiation and connection failures use `video_negotiation_failed` / `video_connection_failed`; applications should recover or close the view explicitly. There is no automatic JPEG fallback.
126
+
105
127
  ## Live Browser Canvas
106
128
 
107
129
  Import the browser-only renderer from `@push.rocks/smartbrowser/web`. The Node.js root entry is intentionally separate and must not be imported into a frontend bundle.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartbrowser',
6
- version: '4.2.0',
6
+ version: '4.3.0',
7
7
  description: 'A simplified Puppeteer wrapper for easy automation and testing tasks.'
8
8
  }
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartbrowser',
6
- version: '4.2.0',
6
+ version: '4.3.0',
7
7
  description: 'A simplified Puppeteer wrapper for easy automation and testing tasks.'
8
8
  }