@push.rocks/smartbrowser 4.1.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/classes.livebrowsercanvasrenderer.d.ts +4 -187
- package/dist_ts_web/classes.livebrowsercanvasrenderer.js +5 -1750
- package/dist_ts_web/classes.livebrowserrenderer.d.ts +194 -0
- package/dist_ts_web/classes.livebrowserrenderer.js +1830 -0
- package/dist_ts_web/classes.livebrowservideoreceiver.d.ts +46 -0
- package/dist_ts_web/classes.livebrowservideoreceiver.js +243 -0
- package/dist_ts_web/classes.livebrowservideorenderer.d.ts +6 -0
- package/dist_ts_web/classes.livebrowservideorenderer.js +8 -0
- package/dist_ts_web/index.d.ts +3 -0
- package/dist_ts_web/index.js +2 -1
- package/dist_ts_web/interfaces.livebrowsercanvas.d.ts +8 -2
- package/dist_ts_web/interfaces.livebrowservideo.d.ts +31 -0
- package/dist_ts_web/interfaces.livebrowservideo.js +2 -0
- package/package.json +4 -4
- package/readme.md +25 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/classes.livebrowsercanvasrenderer.ts +5 -2151
- package/ts_web/classes.livebrowserrenderer.ts +2232 -0
- package/ts_web/classes.livebrowservideoreceiver.ts +250 -0
- package/ts_web/classes.livebrowservideorenderer.ts +9 -0
- package/ts_web/index.ts +4 -0
- package/ts_web/interfaces.livebrowsercanvas.ts +11 -1
- package/ts_web/interfaces.livebrowservideo.ts +46 -0
- package/readme.hints.md +0 -34
|
@@ -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=
|
package/dist_ts_web/index.d.ts
CHANGED
|
@@ -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';
|
package/dist_ts_web/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { LiveBrowserCanvasRenderer } from './classes.livebrowsercanvasrenderer.js';
|
|
2
|
-
|
|
2
|
+
export { LiveBrowserVideoRenderer } from './classes.livebrowservideorenderer.js';
|
|
3
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c193ZWIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLHlCQUF5QixFQUFFLE1BQU0sd0NBQXdDLENBQUM7QUE0Qm5GLE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLHVDQUF1QyxDQUFDIn0=
|
|
@@ -11,17 +11,23 @@ export interface ILiveBrowserCanvasClient {
|
|
|
11
11
|
* It does not prove that the upstream frame was retired.
|
|
12
12
|
*/
|
|
13
13
|
acknowledgeFrame(requestArg: ILiveBrowserFrameAcknowledgementRequest, optionsArg: ILiveBrowserCanvasOperationOptions): Promise<ILiveBrowserFrameAcknowledgement>;
|
|
14
|
-
|
|
14
|
+
/** Shared hosts return their effective viewport, which may be constrained by another viewer. */
|
|
15
|
+
setViewport(viewportArg: ILiveBrowserViewport, optionsArg: ILiveBrowserCanvasOperationOptions): Promise<void | ILiveBrowserCanvasViewportResult>;
|
|
15
16
|
dispatchMouse(inputArg: ILiveBrowserMouseInput, optionsArg: ILiveBrowserCanvasOperationOptions): Promise<void>;
|
|
16
17
|
dispatchWheel(inputArg: ILiveBrowserWheelInput, optionsArg: ILiveBrowserCanvasOperationOptions): Promise<void>;
|
|
17
18
|
dispatchKey(inputArg: ILiveBrowserKeyInput, optionsArg: ILiveBrowserCanvasOperationOptions): Promise<void>;
|
|
18
19
|
insertText(inputArg: ILiveBrowserInsertTextInput, optionsArg: ILiveBrowserCanvasOperationOptions): Promise<void>;
|
|
19
20
|
}
|
|
21
|
+
export interface ILiveBrowserCanvasViewportResult {
|
|
22
|
+
viewport: ILiveBrowserViewport;
|
|
23
|
+
/** The authoritative revision, including when the aggregate viewport did not change. */
|
|
24
|
+
viewportRevision: number;
|
|
25
|
+
}
|
|
20
26
|
export interface ILiveBrowserCanvasOperationOptions {
|
|
21
27
|
/** Adapters must propagate this signal and reject promptly when it aborts. */
|
|
22
28
|
signal: AbortSignal;
|
|
23
29
|
}
|
|
24
|
-
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';
|
|
25
31
|
export interface ILiveBrowserCanvasError {
|
|
26
32
|
code: TLiveBrowserCanvasErrorCode;
|
|
27
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartbrowser",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
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.
|
|
38
|
+
"@git.zone/tsbuild": "^4.4.3",
|
|
39
39
|
"@git.zone/tsrun": "^2.0.6",
|
|
40
|
-
"@git.zone/tstest": "^
|
|
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
|
|
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.
|
|
@@ -171,6 +193,8 @@ Pointer and wheel input are captured from the canvas. Keyboard and best-effort `
|
|
|
171
193
|
|
|
172
194
|
When `resizeTarget` is supplied, resize updates are deduplicated, serialized, and fenced by viewport revision. Sizes observed through `ResizeObserver` are debounced for about 100 ms (trailing), so a drag resize produces one `setViewport()` call for the final size; `syncViewport()` and window resize events measure immediately. Positive fractional dimensions are rounded to at least one CSS pixel, oversized dimensions are reduced proportionally to the 4,096 x 4,096 ceiling, and the device scale factor is clamped to 0.25 through 3 before being reduced further when necessary to stay within the physical-pixel ceiling. The target must have stable CSS dimensions that do not depend on `canvas.width` or `canvas.height`; this prevents intrinsic canvas updates from causing resize feedback.
|
|
173
195
|
|
|
196
|
+
Shared browser hosts may return `{ viewport, viewportRevision }` from `setViewport()` to acknowledge the effective viewport after combining viewer preferences. The renderer accepts a smaller viewport or an unchanged revision, remembers its own requested size separately, and enables input only when a current displayed frame matches the accepted revision. A frame received before the viewport response can satisfy that fence. A newer authoritative viewport revision supersedes a pending shared resize. Existing clients returning `void` retain the single-view contract: apply the requested size and advance the viewport revision. Input remains blocked while the request settles; a still-current image stays visible when the aggregate size does not change.
|
|
197
|
+
|
|
174
198
|
`ILiveBrowserCanvasRendererOptions` supports:
|
|
175
199
|
|
|
176
200
|
- `canvas` and `client`: required rendering and transport-adapter dependencies.
|
package/ts/00_commitinfo_data.ts
CHANGED