@linkshell/host 0.4.44 → 0.4.46
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/host/src/drivers/acp/connection.js +2 -7
- package/dist/host/src/drivers/acp/connection.js.map +1 -1
- package/dist/host/src/drivers/acp/driver.js +2 -4
- package/dist/host/src/drivers/acp/driver.js.map +1 -1
- package/dist/host/src/drivers/auth.js +1 -3
- package/dist/host/src/drivers/auth.js.map +1 -1
- package/dist/host/src/drivers/claude/driver.js +23 -11
- package/dist/host/src/drivers/claude/driver.js.map +1 -1
- package/dist/host/src/drivers/codex/app-server.d.ts +6 -0
- package/dist/host/src/drivers/codex/app-server.js +44 -21
- package/dist/host/src/drivers/codex/app-server.js.map +1 -1
- package/dist/host/src/drivers/codex/driver.d.ts +1 -0
- package/dist/host/src/drivers/codex/driver.js +11 -7
- package/dist/host/src/drivers/codex/driver.js.map +1 -1
- package/dist/host/src/drivers/codex/installation.d.ts +2 -0
- package/dist/host/src/drivers/codex/installation.js +29 -0
- package/dist/host/src/drivers/codex/installation.js.map +1 -0
- package/dist/host/src/fs.d.ts +13 -0
- package/dist/host/src/fs.js +29 -2
- package/dist/host/src/fs.js.map +1 -1
- package/dist/host/src/host.d.ts +1 -0
- package/dist/host/src/host.js +6 -4
- package/dist/host/src/host.js.map +1 -1
- package/dist/host/src/input.d.ts +29 -2
- package/dist/host/src/input.js +113 -5
- package/dist/host/src/input.js.map +1 -1
- package/dist/host/src/local-transport.d.ts +5 -0
- package/dist/host/src/local-transport.js +22 -0
- package/dist/host/src/local-transport.js.map +1 -0
- package/dist/host/src/ports.js +10 -1
- package/dist/host/src/ports.js.map +1 -1
- package/dist/host/src/process.d.ts +12 -0
- package/dist/host/src/process.js +59 -0
- package/dist/host/src/process.js.map +1 -0
- package/dist/host/src/rpc/client.js +2 -3
- package/dist/host/src/rpc/client.js.map +1 -1
- package/dist/host/src/rpc/server.d.ts +1 -0
- package/dist/host/src/rpc/server.js +12 -3
- package/dist/host/src/rpc/server.js.map +1 -1
- package/dist/host/src/screen-h264.d.ts +15 -0
- package/dist/host/src/screen-h264.js +88 -0
- package/dist/host/src/screen-h264.js.map +1 -0
- package/dist/host/src/screen-pacer.d.ts +7 -1
- package/dist/host/src/screen-pacer.js +11 -5
- package/dist/host/src/screen-pacer.js.map +1 -1
- package/dist/host/src/screen-session.d.ts +35 -0
- package/dist/host/src/screen-session.js +26 -0
- package/dist/host/src/screen-session.js.map +1 -0
- package/dist/host/src/screen-viewer.d.ts +1 -1
- package/dist/host/src/screen-viewer.js +373 -42
- package/dist/host/src/screen-viewer.js.map +1 -1
- package/dist/host/src/screen.d.ts +3 -1
- package/dist/host/src/screen.js +560 -165
- package/dist/host/src/screen.js.map +1 -1
- package/dist/host/src/terminals.js +6 -3
- package/dist/host/src/terminals.js.map +1 -1
- package/dist/host/src/windows-screen.d.ts +38 -0
- package/dist/host/src/windows-screen.js +80 -0
- package/dist/host/src/windows-screen.js.map +1 -0
- package/dist/host/src/windows-video.d.ts +78 -0
- package/dist/host/src/windows-video.js +368 -0
- package/dist/host/src/windows-video.js.map +1 -0
- package/dist/host/src/worktrees.js +2 -2
- package/dist/host/src/worktrees.js.map +1 -1
- package/dist/host/tsconfig.tsbuildinfo +1 -1
- package/dist/wire/src/rpc.d.ts +49 -0
- package/dist/wire/src/rpc.js +18 -0
- package/dist/wire/src/rpc.js.map +1 -1
- package/package.json +8 -6
package/dist/host/src/screen.js
CHANGED
|
@@ -9,6 +9,9 @@ import { closeInputApp, inputApp, InputControl, macAppSupported, NO_APP, videoSi
|
|
|
9
9
|
import { LADDER, Pacer, RELAY_LEVEL, rung } from "./screen-pacer.js";
|
|
10
10
|
import { viewerPage } from "./screen-viewer.js";
|
|
11
11
|
import { screenMeasurementLog } from "./screen-metrics.js";
|
|
12
|
+
import { captureWindows, configureWindowsCapture, WindowsFrameSplitter, windowsDisplays, windowsHelper, windowsProfile } from "./windows-screen.js";
|
|
13
|
+
import { WindowsVideoSession, windowsMedia } from "./windows-video.js";
|
|
14
|
+
import { fallbackProfile, fallbackSize, SCREEN_RETAIN_MS, screenConfiguration } from "./screen-session.js";
|
|
12
15
|
// Viewing the computer's screen from the phone. The host serves a small
|
|
13
16
|
// viewer page on a loopback port; the phone opens it through the same
|
|
14
17
|
// encrypted forwarder as port previews. A random token gates the port: other
|
|
@@ -30,9 +33,18 @@ async function hasFfmpeg() {
|
|
|
30
33
|
}
|
|
31
34
|
/** What the phone is told when the picture can't be had for want of the permission. */
|
|
32
35
|
const notAllowed = (app) => `电脑还没有允许${app ? `「${app}」` : ""}录制屏幕。在电脑上运行 linkshell screen 按提示操作,或在「系统设置 › 隐私与安全性 › 录屏与系统录音」里打开${app ? `「${app}」` : "它"}的开关,然后重新进入这个页面。`;
|
|
36
|
+
async function captureReleased(capture) {
|
|
37
|
+
let timer;
|
|
38
|
+
try {
|
|
39
|
+
await Promise.race([capture.gone, new Promise((resolve) => { timer = setTimeout(resolve, 3000); timer.unref(); })]);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
if (timer)
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
33
46
|
/** ffmpeg's capture of an X display (Linux), for one rung of the ladder (see `screen-pacer.ts`): 0 is the best picture. */
|
|
34
|
-
export function captureArgs(level = 0) {
|
|
35
|
-
const profile = rung(level);
|
|
47
|
+
export function captureArgs(level = 0, profile = fallbackProfile(level)) {
|
|
36
48
|
const rate = (bits) => `${Math.round(bits / 1000)}k`;
|
|
37
49
|
return [
|
|
38
50
|
"-hide_banner",
|
|
@@ -207,6 +219,7 @@ export class ScreenShare {
|
|
|
207
219
|
/** The viewer the app is sending the screen to as a video track. One at a time, like captures. */
|
|
208
220
|
video;
|
|
209
221
|
controls = new Set();
|
|
222
|
+
viewers = new Map();
|
|
210
223
|
constructor(log,
|
|
211
224
|
/** The servers both ends ask for their public address, to reach each other directly. */
|
|
212
225
|
iceServers = () => []) {
|
|
@@ -224,18 +237,22 @@ export class ScreenShare {
|
|
|
224
237
|
return { port, token: this.token, displays: displays.map(({ index, name }) => ({ index, name })) };
|
|
225
238
|
}
|
|
226
239
|
async listDisplays() {
|
|
240
|
+
if (process.platform === "win32")
|
|
241
|
+
return (await windowsDisplays()).map(({ screen, name, w, h }) => ({ index: screen, screen, name, w, h }));
|
|
227
242
|
if (process.platform === "darwin") {
|
|
228
243
|
const app = inputApp(this.log);
|
|
229
244
|
if (!app)
|
|
230
245
|
throw RpcError.app("not_supported", NO_APP);
|
|
231
246
|
const displays = await app.displays();
|
|
232
|
-
return displays.map(({ screen, name }) => ({ index: screen, screen, name: name || `屏幕 ${screen + 1}
|
|
247
|
+
return displays.map(({ screen, name, w, h }) => ({ index: screen, screen, name: name || `屏幕 ${screen + 1}`, w, h }));
|
|
233
248
|
}
|
|
234
249
|
if (process.platform !== "linux")
|
|
235
250
|
throw RpcError.app("not_supported", "这台电脑的系统暂不支持查看屏幕");
|
|
236
251
|
if (!(await hasFfmpeg()))
|
|
237
252
|
throw RpcError.app("not_supported", "查看屏幕需要电脑上装有 ffmpeg");
|
|
238
|
-
|
|
253
|
+
// x11grab captures the X display's root window, which may span several monitors.
|
|
254
|
+
const dimensions = await run("xdpyinfo", [], { timeout: 5000 }).then(({ stdout }) => /dimensions:\s+(\d+)x(\d+)\s+pixels/.exec(stdout), () => null);
|
|
255
|
+
return [{ index: 0, name: "屏幕", screen: 0, ...(dimensions ? { w: Number(dimensions[1]), h: Number(dimensions[2]) } : {}) }];
|
|
239
256
|
}
|
|
240
257
|
listen() {
|
|
241
258
|
if (this.server)
|
|
@@ -252,7 +269,7 @@ export class ScreenShare {
|
|
|
252
269
|
response.writeHead(302, { location: `${url.pathname}${url.search}`, "cache-control": "no-store" }).end();
|
|
253
270
|
return;
|
|
254
271
|
}
|
|
255
|
-
response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(viewerPage());
|
|
272
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(viewerPage(process.platform));
|
|
256
273
|
});
|
|
257
274
|
const sockets = new WebSocketServer({ noServer: true });
|
|
258
275
|
server.on("upgrade", (request, socket, head) => {
|
|
@@ -289,6 +306,16 @@ export class ScreenShare {
|
|
|
289
306
|
* (`ffmpeg`: whether what captures is there — ffmpeg itself on Linux.)
|
|
290
307
|
*/
|
|
291
308
|
async access(ask) {
|
|
309
|
+
if (process.platform === "win32") {
|
|
310
|
+
const found = Boolean(windowsHelper());
|
|
311
|
+
try {
|
|
312
|
+
const displays = await windowsDisplays();
|
|
313
|
+
return { supported: true, ffmpeg: true, recording: displays.length > 0, control: true, app: "LinkShell" };
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
return { supported: true, ffmpeg: found, recording: false, control: false, problem: error.message };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
292
319
|
const supported = macAppSupported() || process.platform === "linux";
|
|
293
320
|
if (process.platform !== "darwin")
|
|
294
321
|
return { supported, ffmpeg: supported && (await hasFfmpeg()), recording: null, control: null, problem: "controlling the screen needs macOS" };
|
|
@@ -310,7 +337,7 @@ export class ScreenShare {
|
|
|
310
337
|
}
|
|
311
338
|
}
|
|
312
339
|
/** Starts a capture where it may record. Undefined when it may not: the viewer has been told why. */
|
|
313
|
-
async capture(screen, level, on) {
|
|
340
|
+
async capture(screen, level, on, profile = fallbackProfile(level), wanted = () => true) {
|
|
314
341
|
let ended = false;
|
|
315
342
|
let leave = () => { };
|
|
316
343
|
const gone = new Promise((resolve) => (leave = resolve));
|
|
@@ -321,6 +348,8 @@ export class ScreenShare {
|
|
|
321
348
|
return undefined;
|
|
322
349
|
}
|
|
323
350
|
const status = await app.access();
|
|
351
|
+
if (!wanted())
|
|
352
|
+
return undefined;
|
|
324
353
|
if (!status.recording) {
|
|
325
354
|
// Someone may be at the computer: the app's window for its permissions comes up there.
|
|
326
355
|
void app.ask().catch(() => { });
|
|
@@ -328,9 +357,9 @@ export class ScreenShare {
|
|
|
328
357
|
return undefined;
|
|
329
358
|
}
|
|
330
359
|
let size;
|
|
331
|
-
const stream = await app.stream(screen,
|
|
360
|
+
const stream = await app.stream(screen, profile, {
|
|
332
361
|
frame: (unit, key, generation) => {
|
|
333
|
-
if (ended)
|
|
362
|
+
if (ended || !wanted())
|
|
334
363
|
return;
|
|
335
364
|
if (size !== undefined && generation !== size)
|
|
336
365
|
on.resized?.();
|
|
@@ -339,37 +368,54 @@ export class ScreenShare {
|
|
|
339
368
|
},
|
|
340
369
|
exit: (error) => {
|
|
341
370
|
leave();
|
|
342
|
-
if (ended)
|
|
371
|
+
if (ended || !wanted())
|
|
343
372
|
return;
|
|
344
373
|
if (error)
|
|
345
374
|
this.log(`[screen] the app's capture ended: ${error}`);
|
|
346
375
|
on.exit(!error ? undefined : /permission|not allowed|not authorized|denied|declined/i.test(error) ? notAllowed(status.app) : "屏幕捕获失败");
|
|
347
376
|
},
|
|
348
377
|
});
|
|
378
|
+
const ready = stream.ready.then(({ width, height, fps }) => ({ width, height, maxFps: fps }));
|
|
379
|
+
void ready.catch(() => { });
|
|
349
380
|
return {
|
|
350
381
|
on,
|
|
351
382
|
gone,
|
|
383
|
+
ready,
|
|
352
384
|
end: () => {
|
|
353
385
|
ended = true;
|
|
354
386
|
stream.end();
|
|
355
387
|
leave();
|
|
356
388
|
},
|
|
357
|
-
change: (next) => stream.set(rung(next)),
|
|
389
|
+
change: (next, requested) => stream.set(requested ?? rung(next)),
|
|
358
390
|
key: () => stream.key(),
|
|
359
391
|
};
|
|
360
392
|
}
|
|
361
|
-
|
|
393
|
+
if (!wanted())
|
|
394
|
+
return undefined;
|
|
395
|
+
const windows = process.platform === "win32";
|
|
396
|
+
const capture = windows ? captureWindows(screen, { ...profile, quality: windowsProfile(level).quality }) : spawn("ffmpeg", captureArgs(level, profile), { stdio: ["ignore", "pipe", "pipe"] });
|
|
362
397
|
this.captures.add(capture);
|
|
363
398
|
const splitter = new AccessUnitSplitter();
|
|
399
|
+
const jpeg = new WindowsFrameSplitter();
|
|
364
400
|
let errors = "";
|
|
365
401
|
capture.stdout.on("data", (chunk) => {
|
|
366
|
-
if (!
|
|
367
|
-
|
|
402
|
+
if (ended || !wanted())
|
|
403
|
+
return;
|
|
404
|
+
try {
|
|
405
|
+
if (windows)
|
|
406
|
+
jpeg.push(chunk, frame => on.frame(frame, true));
|
|
407
|
+
else
|
|
408
|
+
splitter.push(chunk, on.frame);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
errors = error.message;
|
|
412
|
+
endCapture(capture, true);
|
|
413
|
+
}
|
|
368
414
|
});
|
|
369
415
|
capture.stderr.on("data", (chunk) => (errors = (errors + chunk.toString()).slice(-2000)));
|
|
370
416
|
const finished = (code, said) => {
|
|
371
417
|
leave();
|
|
372
|
-
if (ended)
|
|
418
|
+
if (ended || !wanted())
|
|
373
419
|
return;
|
|
374
420
|
if (code)
|
|
375
421
|
this.log(`[screen] capture exited ${code}: ${said.trim()}`);
|
|
@@ -387,11 +433,17 @@ export class ScreenShare {
|
|
|
387
433
|
ended = true;
|
|
388
434
|
endCapture(capture);
|
|
389
435
|
},
|
|
436
|
+
...(windows ? {
|
|
437
|
+
change: (next, requested) => configureWindowsCapture(capture, { t: "profile", ...(requested ?? profile), quality: windowsProfile(next).quality }),
|
|
438
|
+
key: () => configureWindowsCapture(capture, { t: "keyframe" }),
|
|
439
|
+
} : {}),
|
|
390
440
|
};
|
|
391
441
|
}
|
|
392
442
|
/** Takes the screen for a new capture: whatever holds it is ended, and has left, before the new one starts. */
|
|
393
|
-
take(screen, level, on, wanted) {
|
|
443
|
+
take(screen, level, on, wanted, profile = fallbackProfile(level)) {
|
|
394
444
|
const mine = this.turn.then(async () => {
|
|
445
|
+
if (!wanted())
|
|
446
|
+
return undefined;
|
|
395
447
|
this.video?.taken();
|
|
396
448
|
const previous = this.holder;
|
|
397
449
|
if (previous) {
|
|
@@ -400,11 +452,15 @@ export class ScreenShare {
|
|
|
400
452
|
// Another device's picture ends here; a viewer changing its own just carries on.
|
|
401
453
|
if (previous.on !== on)
|
|
402
454
|
previous.on.exit("屏幕画面被另一台设备接手了");
|
|
403
|
-
await
|
|
455
|
+
await captureReleased(previous);
|
|
404
456
|
}
|
|
405
457
|
if (!wanted())
|
|
406
458
|
return undefined;
|
|
407
|
-
const running = await this.capture(screen, level, on);
|
|
459
|
+
const running = await this.capture(screen, level, on, profile, wanted);
|
|
460
|
+
if (!wanted()) {
|
|
461
|
+
running?.end();
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
408
464
|
this.holder = running;
|
|
409
465
|
return running;
|
|
410
466
|
});
|
|
@@ -416,6 +472,56 @@ export class ScreenShare {
|
|
|
416
472
|
* app without the media engine): the picture then goes down the socket.
|
|
417
473
|
*/
|
|
418
474
|
async offerVideo(screen, maxWidth, maxFps, diagnostics, codec, viewer) {
|
|
475
|
+
if (process.platform === "win32" && process.env.LINKSHELL_SCREEN_VIDEO !== "off" && windowsMedia()) {
|
|
476
|
+
// The native guard checks the selected desktop and the local opt-out throughout capture.
|
|
477
|
+
await windowsDisplays();
|
|
478
|
+
if (viewer.closed())
|
|
479
|
+
return "refused";
|
|
480
|
+
this.video?.taken();
|
|
481
|
+
const holder = this.holder;
|
|
482
|
+
if (holder) {
|
|
483
|
+
this.holder = undefined;
|
|
484
|
+
holder.end();
|
|
485
|
+
holder.on.exit("屏幕画面被另一台设备接手了");
|
|
486
|
+
await captureReleased(holder);
|
|
487
|
+
}
|
|
488
|
+
if (viewer.closed())
|
|
489
|
+
return "refused";
|
|
490
|
+
const iceServers = this.iceServers().map(url => ({ urls: [url] }));
|
|
491
|
+
viewer.tell({ rtc: { t: "config", iceServers } });
|
|
492
|
+
const session = new WindowsVideoSession({ screen, iceServers, maxWidth, maxFps }, {
|
|
493
|
+
tell: message => viewer.tell({ rtc: message }), input: viewer.input, lost: viewer.lost, log: this.log,
|
|
494
|
+
});
|
|
495
|
+
const mine = { id: session.id, taken: () => {
|
|
496
|
+
if (this.video === mine)
|
|
497
|
+
this.video = undefined;
|
|
498
|
+
session.end();
|
|
499
|
+
viewer.refuse("屏幕画面被另一台设备接手了");
|
|
500
|
+
} };
|
|
501
|
+
this.video = mine;
|
|
502
|
+
const cancelled = setInterval(() => { if (viewer.closed())
|
|
503
|
+
session.end(); }, 100);
|
|
504
|
+
try {
|
|
505
|
+
await session.offer();
|
|
506
|
+
if (viewer.closed() || this.video !== mine) {
|
|
507
|
+
session.end();
|
|
508
|
+
if (this.video === mine)
|
|
509
|
+
this.video = undefined;
|
|
510
|
+
return "refused";
|
|
511
|
+
}
|
|
512
|
+
return session;
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
session.end();
|
|
516
|
+
if (this.video === mine)
|
|
517
|
+
this.video = undefined;
|
|
518
|
+
this.log(`[screen] Windows video could not start: ${String(error)}`);
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
clearInterval(cancelled);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
419
525
|
// LINKSHELL_SCREEN_VIDEO=off: every viewer gets the picture down the socket (to compare the two, or should the track ever misbehave).
|
|
420
526
|
const app = process.platform === "darwin" && process.env.LINKSHELL_SCREEN_VIDEO !== "off" ? inputApp(this.log) : undefined;
|
|
421
527
|
if (!app)
|
|
@@ -435,10 +541,14 @@ export class ScreenShare {
|
|
|
435
541
|
this.video?.taken();
|
|
436
542
|
const holder = this.holder;
|
|
437
543
|
if (holder) {
|
|
438
|
-
this.holder = undefined;
|
|
439
544
|
holder.end();
|
|
440
545
|
holder.on.exit("屏幕画面被另一台设备接手了");
|
|
546
|
+
await captureReleased(holder);
|
|
547
|
+
if (this.holder === holder)
|
|
548
|
+
this.holder = undefined;
|
|
441
549
|
}
|
|
550
|
+
if (viewer.closed())
|
|
551
|
+
return "refused";
|
|
442
552
|
const id = `video-${randomBytes(6).toString("hex")}`;
|
|
443
553
|
const mine = {
|
|
444
554
|
id,
|
|
@@ -480,44 +590,153 @@ export class ScreenShare {
|
|
|
480
590
|
viewer.tell({ rtc: { ...rest, t: kind.slice(4) } });
|
|
481
591
|
}
|
|
482
592
|
});
|
|
483
|
-
|
|
593
|
+
if (viewer.closed() || this.video !== mine) {
|
|
594
|
+
app.endVideo(id);
|
|
595
|
+
if (this.video === mine)
|
|
596
|
+
this.video = undefined;
|
|
597
|
+
return "refused";
|
|
598
|
+
}
|
|
599
|
+
return { id, reconfigure: status.videoReconfigure === true, configure: configuration => app.configureVideo(id, configuration), signal: message => app.signal(id, message), end: () => app.endVideo(id) };
|
|
484
600
|
}
|
|
485
601
|
stream(ws, display, relayed, wantsVideo, width, maxFps, diagnostics = false, codec) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
// The picture: as good as the path carries without falling behind (see `screen-pacer.ts`).
|
|
496
|
-
const best = relayed ? RELAY_LEVEL : 0;
|
|
497
|
-
const pacer = new Pacer(Date.now());
|
|
498
|
-
let level = best;
|
|
602
|
+
// Paused viewers still own a session. A new authenticated viewer takes that ownership too.
|
|
603
|
+
for (const [previous, release] of this.viewers) {
|
|
604
|
+
if (previous.readyState === previous.OPEN)
|
|
605
|
+
previous.send(JSON.stringify({ error: "屏幕画面被另一台设备接手了" }));
|
|
606
|
+
release();
|
|
607
|
+
previous.close();
|
|
608
|
+
}
|
|
609
|
+
let shown = this.displays.find((entry) => entry.index === display) ?? this.displays[0];
|
|
610
|
+
let desired = { display: shown?.index ?? 0, width: width ?? 1920, maxFps: maxFps === 30 || maxFps === 60 || maxFps === 120 ? maxFps : undefined, paused: false };
|
|
499
611
|
let closed = false;
|
|
612
|
+
const live = () => !closed && ws.readyState === ws.OPEN;
|
|
613
|
+
const tell = (message) => { if (live())
|
|
614
|
+
ws.send(JSON.stringify(message)); };
|
|
615
|
+
let control;
|
|
616
|
+
let controlRequested = false;
|
|
617
|
+
let inputEnabled = false;
|
|
618
|
+
let generationEnabled = false;
|
|
619
|
+
let inputGeneration = 0;
|
|
620
|
+
let appliedGeneration;
|
|
621
|
+
let revision = 0;
|
|
622
|
+
let lastRequestId = -1;
|
|
623
|
+
let desiredRequestId;
|
|
624
|
+
let helperRequestId = 0;
|
|
625
|
+
let pending;
|
|
626
|
+
let configuring = false;
|
|
627
|
+
let initialized = false;
|
|
628
|
+
let canConfigure = false;
|
|
629
|
+
let pausedExpiry;
|
|
630
|
+
let running;
|
|
631
|
+
let captureGeneration = 0;
|
|
632
|
+
let video;
|
|
633
|
+
let signals = Promise.resolve();
|
|
634
|
+
const windows = process.platform === "win32";
|
|
635
|
+
const best = relayed ? windows ? 1 : RELAY_LEVEL : 0;
|
|
636
|
+
const pacer = new Pacer(Date.now(), windows ? { maxInFlight: 2, behindMs: 120, caughtUpMs: 50, probeMs: 1500 } : {});
|
|
637
|
+
const profileFor = (next, configuration) => ({
|
|
638
|
+
...fallbackProfile(next, configuration),
|
|
639
|
+
...(windows ? windowsProfile(next, configuration.width, configuration.maxFps) : {}),
|
|
640
|
+
});
|
|
641
|
+
let level = best;
|
|
500
642
|
let changing = false;
|
|
501
643
|
let lighter = false;
|
|
502
644
|
let askedForKey = 0;
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
645
|
+
let wantsKey = true;
|
|
646
|
+
const stopControl = () => {
|
|
647
|
+
inputEnabled = false;
|
|
648
|
+
if (!control)
|
|
649
|
+
return;
|
|
650
|
+
control.stop();
|
|
651
|
+
this.controls.delete(control);
|
|
652
|
+
control = undefined;
|
|
653
|
+
};
|
|
654
|
+
const startControl = async (preparing = false) => {
|
|
655
|
+
if (!live() || (!inputEnabled && !preparing) || desired.paused || !controlRequested || control)
|
|
656
|
+
return;
|
|
657
|
+
const at = revision;
|
|
658
|
+
const created = new InputControl({
|
|
659
|
+
// Readiness may arrive while the applied target is still being acknowledged; it is
|
|
660
|
+
// safe to report permissions then, while the independent input gate stays closed.
|
|
661
|
+
state: (state) => { if (control === created && revision === at)
|
|
662
|
+
tell({ control: state }); },
|
|
663
|
+
cursor: (x, y) => { if (control === created && inputEnabled && !video?.cursor?.(x, y))
|
|
664
|
+
tell({ cursor: { x, y } }); },
|
|
665
|
+
posted: (event) => this.log(`[screen] dry run: ${JSON.stringify(event)}`),
|
|
666
|
+
}, this.log);
|
|
667
|
+
control = created;
|
|
668
|
+
this.controls.add(created);
|
|
669
|
+
await created.start(shown?.screen ?? 0);
|
|
670
|
+
if (!live() || control !== created || revision !== at || (!inputEnabled && !preparing)) {
|
|
671
|
+
created.stop();
|
|
672
|
+
this.controls.delete(created);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
const stopRunning = () => {
|
|
676
|
+
captureGeneration += 1;
|
|
677
|
+
const stopped = running;
|
|
678
|
+
running = undefined;
|
|
679
|
+
stopped?.end();
|
|
680
|
+
// ffmpeg can hold the recorder until SIGKILL; the next capture must wait for it.
|
|
681
|
+
if (stopped && this.holder === stopped)
|
|
682
|
+
void stopped.gone.then(() => { if (this.holder === stopped)
|
|
683
|
+
this.holder = undefined; });
|
|
684
|
+
};
|
|
685
|
+
const endVideo = () => {
|
|
686
|
+
if (!video)
|
|
687
|
+
return;
|
|
688
|
+
const ended = video;
|
|
689
|
+
video = undefined;
|
|
690
|
+
ended.end();
|
|
691
|
+
if (this.video?.id === ended.id)
|
|
692
|
+
this.video = undefined;
|
|
693
|
+
};
|
|
694
|
+
const expirePaused = () => {
|
|
695
|
+
if (pausedExpiry)
|
|
696
|
+
return;
|
|
697
|
+
pausedExpiry = setTimeout(() => {
|
|
698
|
+
pausedExpiry = undefined;
|
|
699
|
+
if (live()) {
|
|
700
|
+
dispose();
|
|
701
|
+
ws.close(1000, "screen retention expired");
|
|
702
|
+
}
|
|
703
|
+
}, SCREEN_RETAIN_MS);
|
|
704
|
+
pausedExpiry.unref();
|
|
705
|
+
};
|
|
706
|
+
const resumed = () => {
|
|
707
|
+
if (pausedExpiry)
|
|
708
|
+
clearTimeout(pausedExpiry);
|
|
709
|
+
pausedExpiry = undefined;
|
|
710
|
+
};
|
|
711
|
+
const capabilities = () => tell({ session: { t: "capabilities", version: 1, configure: canConfigure, pause: video ? !!(video.reconfigure || video.pause) : true, retainMs: SCREEN_RETAIN_MS } });
|
|
712
|
+
const failure = (error) => {
|
|
713
|
+
if (!live())
|
|
714
|
+
return;
|
|
715
|
+
this.log(`[screen] the capture did not start: ${String(error)}`);
|
|
716
|
+
tell({ error: "屏幕捕获失败" });
|
|
717
|
+
dispose();
|
|
718
|
+
ws.close();
|
|
719
|
+
};
|
|
509
720
|
const askForKey = () => {
|
|
721
|
+
if (!live() || desired.paused || configuring)
|
|
722
|
+
return;
|
|
510
723
|
const now = Date.now();
|
|
511
724
|
if (!running?.key || now - askedForKey < 500 || !pacer.needsKey(now))
|
|
512
725
|
return;
|
|
513
726
|
askedForKey = now;
|
|
514
727
|
running.key();
|
|
515
728
|
};
|
|
516
|
-
|
|
517
|
-
const
|
|
729
|
+
let reminder;
|
|
730
|
+
const stopReminder = () => { if (reminder)
|
|
731
|
+
clearInterval(reminder); reminder = undefined; };
|
|
732
|
+
const startReminder = () => { if (!reminder)
|
|
733
|
+
reminder = setInterval(askForKey, 500); };
|
|
734
|
+
startReminder();
|
|
518
735
|
const events = {
|
|
519
736
|
frame: (unit, key) => {
|
|
520
|
-
if (
|
|
737
|
+
if (!live() || desired.paused)
|
|
738
|
+
return;
|
|
739
|
+
if (windows && ws.bufferedAmount > 256 * 1024)
|
|
521
740
|
return;
|
|
522
741
|
if (wantsKey && !key)
|
|
523
742
|
return;
|
|
@@ -526,110 +745,239 @@ export class ScreenShare {
|
|
|
526
745
|
const seq = pacer.next(key, now);
|
|
527
746
|
if (seq !== undefined) {
|
|
528
747
|
const head = Buffer.allocUnsafe(5);
|
|
529
|
-
head[0] = key ? 1 : 0;
|
|
748
|
+
head[0] = windows ? 2 : key ? 1 : 0;
|
|
530
749
|
head.writeUInt32BE(seq >>> 0, 1);
|
|
531
750
|
ws.send(Buffer.concat([head, unit]));
|
|
532
751
|
}
|
|
533
752
|
else
|
|
534
753
|
askForKey();
|
|
535
|
-
const advice = changing ? undefined : pacer.advice(now, level > best, level < LADDER.length - 1);
|
|
754
|
+
const advice = changing || configuring ? undefined : pacer.advice(now, level > best, level < LADDER.length - 1);
|
|
536
755
|
if (advice)
|
|
537
|
-
void
|
|
756
|
+
void changeLevel(level + (advice === "down" ? 1 : -1));
|
|
538
757
|
},
|
|
539
|
-
// The page starts its decoder afresh for the new size; what it had of the old one came before this, in order.
|
|
540
758
|
resized: () => tell({ restart: true, level, lighter }),
|
|
541
|
-
exit: (message) => {
|
|
542
|
-
|
|
543
|
-
tell({ error: message });
|
|
544
|
-
ws.close();
|
|
545
|
-
},
|
|
759
|
+
exit: (message) => { if (message)
|
|
760
|
+
tell({ error: message }); dispose(); ws.close(); },
|
|
546
761
|
};
|
|
547
|
-
const begin = async () => {
|
|
548
|
-
const
|
|
549
|
-
|
|
762
|
+
const begin = async (at, target, configuration) => {
|
|
763
|
+
const captureAt = ++captureGeneration;
|
|
764
|
+
const wanted = () => live() && revision === at && captureGeneration === captureAt && !desired.paused && !video;
|
|
765
|
+
const scoped = {
|
|
766
|
+
frame: (unit, key) => { if (wanted())
|
|
767
|
+
events.frame(unit, key); },
|
|
768
|
+
resized: () => { if (wanted())
|
|
769
|
+
events.resized?.(); },
|
|
770
|
+
exit: (message) => { if (wanted())
|
|
771
|
+
events.exit(message); },
|
|
772
|
+
};
|
|
773
|
+
const started = await this.take(target?.screen ?? 0, level, scoped, wanted, profileFor(level, configuration));
|
|
774
|
+
if (!wanted()) {
|
|
775
|
+
if (this.holder === started)
|
|
776
|
+
this.holder = undefined;
|
|
550
777
|
started?.end();
|
|
551
|
-
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
552
780
|
running = started;
|
|
781
|
+
if (started?.ready) {
|
|
782
|
+
let timer;
|
|
783
|
+
try {
|
|
784
|
+
const size = await Promise.race([started.ready, new Promise((_resolve, reject) => { timer = setTimeout(() => reject(new Error("屏幕捕获启动超时")), 15_000); timer.unref(); })]);
|
|
785
|
+
if (wanted())
|
|
786
|
+
return size;
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
if (wanted())
|
|
790
|
+
throw error;
|
|
791
|
+
}
|
|
792
|
+
finally {
|
|
793
|
+
if (timer)
|
|
794
|
+
clearTimeout(timer);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return undefined;
|
|
553
799
|
};
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
ws.close();
|
|
558
|
-
};
|
|
559
|
-
const change = async (next) => {
|
|
800
|
+
const changeLevel = async (next) => {
|
|
801
|
+
if (!live() || desired.paused || configuring || changing)
|
|
802
|
+
return;
|
|
560
803
|
changing = true;
|
|
561
804
|
lighter = next > level;
|
|
562
|
-
|
|
563
|
-
this.log(`[screen] picture ${lighter ? "lighter" : "better"}: ${to.width} wide, ${to.fps} a second, ${Math.round(to.bitrate / 1000)} kbit/s`);
|
|
805
|
+
level = next;
|
|
564
806
|
if (running?.change) {
|
|
565
|
-
|
|
566
|
-
running.change(next);
|
|
567
|
-
level = next;
|
|
807
|
+
running.change(next, profileFor(next, desired));
|
|
568
808
|
pacer.changed(Date.now());
|
|
569
|
-
changing = false;
|
|
570
|
-
return;
|
|
571
809
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
810
|
+
else {
|
|
811
|
+
stopRunning();
|
|
812
|
+
tell({ restart: true, level, lighter });
|
|
813
|
+
await begin(revision, shown, desired).catch(failure);
|
|
814
|
+
pacer.restarted(Date.now());
|
|
815
|
+
}
|
|
578
816
|
changing = false;
|
|
579
817
|
};
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
let signals = Promise.resolve();
|
|
584
|
-
const endVideo = () => {
|
|
585
|
-
if (!video)
|
|
818
|
+
const current = (change) => live() && revision === change.revision;
|
|
819
|
+
const drain = async () => {
|
|
820
|
+
if (!initialized || configuring || !live())
|
|
586
821
|
return;
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
822
|
+
configuring = true;
|
|
823
|
+
try {
|
|
824
|
+
while (pending && live()) {
|
|
825
|
+
const change = pending;
|
|
826
|
+
pending = undefined;
|
|
827
|
+
const configuration = change.configuration;
|
|
828
|
+
try {
|
|
829
|
+
if (!canConfigure && change.requestId !== undefined)
|
|
830
|
+
throw new Error("电脑上的屏幕组件不支持保留连接,请重新连接");
|
|
831
|
+
// A monitor may have been unplugged since screen.start listed it.
|
|
832
|
+
const displays = await this.listDisplays();
|
|
833
|
+
if (!current(change))
|
|
834
|
+
continue;
|
|
835
|
+
this.displays = displays;
|
|
836
|
+
const target = displays.find((entry) => entry.index === configuration.display);
|
|
837
|
+
if (!target)
|
|
838
|
+
throw new Error("所选屏幕已不可用,请选择其他屏幕");
|
|
839
|
+
let applied;
|
|
840
|
+
if (video) {
|
|
841
|
+
if (!video.reconfigure || !video.configure) {
|
|
842
|
+
// Windows can pause its existing track, but target changes still reconnect.
|
|
843
|
+
if (video.pause && change.requestId === undefined) {
|
|
844
|
+
video.pause(configuration.paused);
|
|
845
|
+
applied = { screen: target.screen, width: target.w ?? 0, height: target.h ?? 0, maxFps: configuration.maxFps ?? 60, paused: configuration.paused };
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
if (configuration.paused) {
|
|
849
|
+
dispose();
|
|
850
|
+
ws.close();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
throw new Error("电脑上的屏幕组件不支持保留连接,请重新连接");
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
else
|
|
857
|
+
applied = await video.configure({
|
|
858
|
+
requestId: ++helperRequestId, screen: target.screen, maxWidth: configuration.width,
|
|
859
|
+
...(configuration.maxFps === undefined ? {} : { maxFps: configuration.maxFps }), paused: configuration.paused,
|
|
860
|
+
...(change.generation === undefined ? {} : { inputGeneration: change.generation }),
|
|
861
|
+
});
|
|
862
|
+
if (applied.screen !== target.screen || applied.paused !== configuration.paused || (change.generation !== undefined && applied.generation !== change.generation)) {
|
|
863
|
+
throw new Error("电脑返回的屏幕设置无效,请重试");
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
const profile = profileFor(level, configuration);
|
|
868
|
+
let size = !target.w || !target.h ? { width: 0, height: 0 } : fallbackSize(target, profile);
|
|
869
|
+
let fps = profile.fps;
|
|
870
|
+
if ((!size.width || !size.height) && change.requestId !== undefined)
|
|
871
|
+
throw new Error("无法读取当前屏幕尺寸,请重新连接");
|
|
872
|
+
stopRunning();
|
|
873
|
+
if (!configuration.paused) {
|
|
874
|
+
wantsKey = true;
|
|
875
|
+
pacer.restarted(Date.now());
|
|
876
|
+
tell({ restart: true, level, lighter: false });
|
|
877
|
+
const started = await begin(change.revision, target, configuration);
|
|
878
|
+
if (started) {
|
|
879
|
+
size = started;
|
|
880
|
+
fps = started.maxFps;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
applied = { screen: target.screen, ...size, maxFps: fps, paused: configuration.paused, generation: change.generation };
|
|
884
|
+
}
|
|
885
|
+
if (!current(change))
|
|
886
|
+
continue;
|
|
887
|
+
shown = target;
|
|
888
|
+
appliedGeneration = change.generation;
|
|
889
|
+
if (!configuration.paused)
|
|
890
|
+
await startControl(true);
|
|
891
|
+
if (!current(change))
|
|
892
|
+
continue;
|
|
893
|
+
inputEnabled = !configuration.paused;
|
|
894
|
+
if (!configuration.paused) {
|
|
895
|
+
resumed();
|
|
896
|
+
startReminder();
|
|
897
|
+
}
|
|
898
|
+
if (change.requestId !== undefined)
|
|
899
|
+
tell({ session: {
|
|
900
|
+
t: "configured", requestId: change.requestId, display: target.index, width: applied.width, height: applied.height,
|
|
901
|
+
maxFps: applied.maxFps, paused: applied.paused,
|
|
902
|
+
...(applied.generation === undefined ? {} : { generation: applied.generation }),
|
|
903
|
+
} });
|
|
904
|
+
}
|
|
905
|
+
catch (error) {
|
|
906
|
+
if (!current(change))
|
|
907
|
+
continue;
|
|
908
|
+
stopControl();
|
|
909
|
+
if (!video)
|
|
910
|
+
stopRunning();
|
|
911
|
+
this.log(`[screen] configuration failed: ${String(error)}`);
|
|
912
|
+
if (change.requestId !== undefined)
|
|
913
|
+
tell({ session: { t: "error", requestId: change.requestId, message: "屏幕设置未能应用,请重试" } });
|
|
914
|
+
else if (configuration.paused) {
|
|
915
|
+
dispose();
|
|
916
|
+
ws.close();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
finally {
|
|
922
|
+
configuring = false;
|
|
923
|
+
}
|
|
591
924
|
};
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
925
|
+
const configure = (configuration, requestId) => {
|
|
926
|
+
desired = configuration;
|
|
927
|
+
if (requestId !== undefined) {
|
|
928
|
+
generationEnabled = true;
|
|
929
|
+
desiredRequestId = requestId;
|
|
930
|
+
}
|
|
931
|
+
const at = ++revision;
|
|
932
|
+
pending = { configuration, revision: at, requestId, ...(generationEnabled ? { generation: ++inputGeneration } : {}) };
|
|
933
|
+
stopControl();
|
|
934
|
+
if (configuration.paused) {
|
|
935
|
+
stopReminder();
|
|
936
|
+
stopRunning();
|
|
937
|
+
expirePaused();
|
|
938
|
+
}
|
|
939
|
+
void drain();
|
|
940
|
+
};
|
|
941
|
+
const startFallback = () => {
|
|
595
942
|
endVideo();
|
|
596
|
-
if (
|
|
943
|
+
if (!live())
|
|
597
944
|
return;
|
|
598
945
|
tell({ rtc: { t: "off" } });
|
|
599
|
-
|
|
946
|
+
canConfigure = !!shown?.w && !!shown?.h;
|
|
947
|
+
initialized = true;
|
|
948
|
+
capabilities();
|
|
949
|
+
if (pending || desiredRequestId !== undefined || desired.paused) {
|
|
950
|
+
configure(desired, desiredRequestId);
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
const at = revision;
|
|
954
|
+
void begin(at, shown, desired).then(() => {
|
|
955
|
+
if (live() && revision === at && !desired.paused) {
|
|
956
|
+
inputEnabled = true;
|
|
957
|
+
void startControl();
|
|
958
|
+
}
|
|
959
|
+
}).catch(failure);
|
|
960
|
+
}
|
|
600
961
|
};
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
if (offered)
|
|
621
|
-
video = offered;
|
|
622
|
-
else {
|
|
623
|
-
tell({ rtc: { t: "off" } });
|
|
624
|
-
return begin();
|
|
625
|
-
}
|
|
626
|
-
})
|
|
627
|
-
.catch(failed);
|
|
628
|
-
}
|
|
629
|
-
else
|
|
630
|
-
void begin().catch(failed);
|
|
962
|
+
const fallBack = () => { if (video)
|
|
963
|
+
startFallback(); };
|
|
964
|
+
const dispose = () => {
|
|
965
|
+
if (closed)
|
|
966
|
+
return;
|
|
967
|
+
closed = true;
|
|
968
|
+
revision += 1;
|
|
969
|
+
pending = undefined;
|
|
970
|
+
stopReminder();
|
|
971
|
+
resumed();
|
|
972
|
+
stopRunning();
|
|
973
|
+
endVideo();
|
|
974
|
+
stopControl();
|
|
975
|
+
this.viewers.delete(ws);
|
|
976
|
+
};
|
|
977
|
+
this.viewers.set(ws, dispose);
|
|
978
|
+
ws.on("close", dispose);
|
|
631
979
|
ws.on("message", (data, binary) => {
|
|
632
|
-
if (binary)
|
|
980
|
+
if (binary || !live())
|
|
633
981
|
return;
|
|
634
982
|
let message;
|
|
635
983
|
try {
|
|
@@ -639,77 +987,124 @@ export class ScreenShare {
|
|
|
639
987
|
return;
|
|
640
988
|
}
|
|
641
989
|
const kind = message?.t;
|
|
642
|
-
if (kind === "
|
|
990
|
+
if (kind === "screen.configure") {
|
|
991
|
+
const parsed = screenConfiguration.safeParse(message);
|
|
992
|
+
if (!parsed.success || !this.displays.some((entry) => entry.index === parsed.data.display)) {
|
|
993
|
+
const requestId = message.requestId;
|
|
994
|
+
tell({ session: { t: "error", ...(typeof requestId === "number" && Number.isSafeInteger(requestId) && requestId >= 0 ? { requestId } : {}), message: "屏幕设置无效,请检查屏幕、分辨率和帧率" } });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (parsed.data.requestId <= lastRequestId)
|
|
998
|
+
return;
|
|
999
|
+
if (initialized && !canConfigure) {
|
|
1000
|
+
tell({ session: { t: "error", requestId: parsed.data.requestId, message: "电脑上的屏幕组件不支持保留连接,请重新连接" } });
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
lastRequestId = parsed.data.requestId;
|
|
1004
|
+
const { t: _type, requestId, ...configuration } = parsed.data;
|
|
1005
|
+
configure(configuration, requestId);
|
|
1006
|
+
}
|
|
1007
|
+
else if (kind === "hidden" || kind === "shown") {
|
|
1008
|
+
const paused = kind === "hidden";
|
|
1009
|
+
if (desired.paused !== paused)
|
|
1010
|
+
configure({ ...desired, paused });
|
|
1011
|
+
}
|
|
1012
|
+
else if (kind === "ack")
|
|
643
1013
|
pacer.ack(Number(message.n), Date.now());
|
|
644
1014
|
else if (kind === "rtc.answer" || kind === "rtc.ice") {
|
|
645
1015
|
const signal = videoSignal.safeParse(message);
|
|
646
1016
|
if (!signal.success)
|
|
647
1017
|
return;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
.then(async () => {
|
|
652
|
-
const data = signal.data;
|
|
653
|
-
if (!video)
|
|
1018
|
+
signals = signals.then(async () => {
|
|
1019
|
+
const target = video;
|
|
1020
|
+
if (!target || !live())
|
|
654
1021
|
return;
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1022
|
+
const data = signal.data;
|
|
1023
|
+
const resolved = data.t === "rtc.ice" ? { ...data, candidate: await resolvedCandidate(data.candidate) } : data;
|
|
1024
|
+
if (live() && video === target)
|
|
1025
|
+
target.signal(resolved);
|
|
1026
|
+
}).catch(() => { });
|
|
659
1027
|
}
|
|
660
|
-
// The viewer's decoder lost its place (it skipped ahead, or failed): it can only carry on from a keyframe.
|
|
661
|
-
// (Named apart from the viewer's own events: whatever isn't one of these kinds is taken for a pointer or key event.)
|
|
662
1028
|
else if (kind === "keyframe") {
|
|
663
1029
|
const now = Date.now();
|
|
664
|
-
if (running?.key && now - askedForKey >= 500) {
|
|
1030
|
+
if (!desired.paused && !configuring && running?.key && now - askedForKey >= 500) {
|
|
665
1031
|
askedForKey = now;
|
|
666
1032
|
running.key();
|
|
667
1033
|
}
|
|
668
1034
|
}
|
|
669
|
-
else if (kind === "hidden")
|
|
670
|
-
unwatched = true;
|
|
671
|
-
else if (kind === "shown") {
|
|
672
|
-
if (!unwatched)
|
|
673
|
-
return;
|
|
674
|
-
unwatched = false;
|
|
675
|
-
wantsKey = true;
|
|
676
|
-
// What was on its way when the page went out of sight is not waited for.
|
|
677
|
-
pacer.restarted(Date.now());
|
|
678
|
-
running?.key?.();
|
|
679
|
-
}
|
|
680
|
-
// Why a viewer gave its track up: what ICE gathered and tried, for this log only.
|
|
681
1035
|
else if (kind === "rtc.diagnosis") {
|
|
682
1036
|
const { reason, ice } = message;
|
|
683
1037
|
this.log(`[screen] the video track was given up (${String(reason ?? "no reason").slice(0, 120)}): ${String(ice || "no ICE statistics").slice(0, 300)}`);
|
|
684
1038
|
}
|
|
685
|
-
|
|
686
|
-
else if (kind === "rtc.failed") {
|
|
687
|
-
this.log(`[screen] no video track for this viewer (${String(message.reason ?? "no reason given").slice(0, 200)}): the picture goes down the socket`);
|
|
1039
|
+
else if (kind === "rtc.failed")
|
|
688
1040
|
fallBack();
|
|
689
|
-
}
|
|
690
|
-
// A viewer measuring (`?measure=1`): its clock against this computer's (it keeps the answer that came back
|
|
691
|
-
// quickest), then what it sees, once a second.
|
|
692
1041
|
else if (kind === "ping")
|
|
693
1042
|
tell({ pong: { n: message.n, at: message.at, now: Date.now() } });
|
|
694
1043
|
else if (kind === "measure")
|
|
695
1044
|
this.log(`[screen] measured: ${screenMeasurementLog(message)}`);
|
|
696
|
-
else if (kind === "control")
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
this.holder = undefined;
|
|
706
|
-
running?.end();
|
|
707
|
-
endVideo();
|
|
708
|
-
control.stop();
|
|
709
|
-
this.controls.delete(control);
|
|
1045
|
+
else if (kind === "control" && !desired.paused) {
|
|
1046
|
+
// Requesting control only records intent. Input still needs the applied generation,
|
|
1047
|
+
// and startup/configuration must finish before the helper can be joined.
|
|
1048
|
+
controlRequested = true;
|
|
1049
|
+
void startControl();
|
|
1050
|
+
}
|
|
1051
|
+
else if (!desired.paused && inputEnabled && (!generationEnabled || message?.screenGeneration === appliedGeneration)) {
|
|
1052
|
+
control?.send(message);
|
|
1053
|
+
}
|
|
710
1054
|
});
|
|
1055
|
+
if (wantsVideo) {
|
|
1056
|
+
let lostWhileOpening = false;
|
|
1057
|
+
void this.offerVideo(shown?.screen ?? 0, width, maxFps, diagnostics, codec, {
|
|
1058
|
+
closed: () => !live(), tell,
|
|
1059
|
+
refuse: (message) => { tell({ error: message }); dispose(); ws.close(); },
|
|
1060
|
+
lost: () => { if (video)
|
|
1061
|
+
fallBack();
|
|
1062
|
+
else
|
|
1063
|
+
lostWhileOpening = true; },
|
|
1064
|
+
input: message => {
|
|
1065
|
+
if (!live() || desired.paused || !inputEnabled)
|
|
1066
|
+
return;
|
|
1067
|
+
controlRequested = true;
|
|
1068
|
+
const at = revision;
|
|
1069
|
+
void startControl().then(() => { if (live() && revision === at && inputEnabled && !desired.paused)
|
|
1070
|
+
control?.send(message); });
|
|
1071
|
+
},
|
|
1072
|
+
}).then((offered) => {
|
|
1073
|
+
if (offered === "refused")
|
|
1074
|
+
return;
|
|
1075
|
+
if (!live()) {
|
|
1076
|
+
offered?.end();
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (!offered) {
|
|
1080
|
+
startFallback();
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
video = offered;
|
|
1084
|
+
if (lostWhileOpening) {
|
|
1085
|
+
startFallback();
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
initialized = true;
|
|
1089
|
+
canConfigure = offered.reconfigure === true;
|
|
1090
|
+
capabilities();
|
|
1091
|
+
if (pending)
|
|
1092
|
+
void drain();
|
|
1093
|
+
else {
|
|
1094
|
+
inputEnabled = true;
|
|
1095
|
+
void startControl();
|
|
1096
|
+
}
|
|
1097
|
+
}).catch(failure);
|
|
1098
|
+
}
|
|
1099
|
+
else
|
|
1100
|
+
startFallback();
|
|
711
1101
|
}
|
|
712
1102
|
stop() {
|
|
1103
|
+
for (const [viewer, release] of this.viewers) {
|
|
1104
|
+
release();
|
|
1105
|
+
viewer.close(1000, "host stopped");
|
|
1106
|
+
}
|
|
1107
|
+
this.viewers.clear();
|
|
713
1108
|
// The host is going: there is no later to insist in.
|
|
714
1109
|
this.holder?.end();
|
|
715
1110
|
this.holder = undefined;
|