linkshell-cli 0.8.1 → 0.8.2
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/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/host/src/screen-pacer.d.ts +62 -0
- package/dist/host/src/screen-pacer.js +143 -0
- package/dist/host/src/screen-pacer.js.map +1 -0
- package/dist/host/src/screen-viewer.js +35 -5
- package/dist/host/src/screen-viewer.js.map +1 -1
- package/dist/host/src/screen.d.ts +12 -24
- package/dist/host/src/screen.js +132 -58
- package/dist/host/src/screen.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** How the picture is sent, best first. A gateway carries messages rather than video: relayed, it starts lower. */
|
|
2
|
+
export declare const LADDER: readonly [{
|
|
3
|
+
readonly fps: 20;
|
|
4
|
+
readonly width: 1600;
|
|
5
|
+
readonly bitrate: "3M";
|
|
6
|
+
readonly ceiling: "4M";
|
|
7
|
+
}, {
|
|
8
|
+
readonly fps: 15;
|
|
9
|
+
readonly width: 1440;
|
|
10
|
+
readonly bitrate: "1500k";
|
|
11
|
+
readonly ceiling: "2M";
|
|
12
|
+
}, {
|
|
13
|
+
readonly fps: 12;
|
|
14
|
+
readonly width: 1280;
|
|
15
|
+
readonly bitrate: "900k";
|
|
16
|
+
readonly ceiling: "1300k";
|
|
17
|
+
}, {
|
|
18
|
+
readonly fps: 10;
|
|
19
|
+
readonly width: 1024;
|
|
20
|
+
readonly bitrate: "500k";
|
|
21
|
+
readonly ceiling: "700k";
|
|
22
|
+
}, {
|
|
23
|
+
readonly fps: 8;
|
|
24
|
+
readonly width: 854;
|
|
25
|
+
readonly bitrate: "260k";
|
|
26
|
+
readonly ceiling: "380k";
|
|
27
|
+
}];
|
|
28
|
+
/** The level a relayed stream starts at, and never goes above. */
|
|
29
|
+
export declare const RELAY_LEVEL = 2;
|
|
30
|
+
export declare class Pacer {
|
|
31
|
+
private seq;
|
|
32
|
+
private shown;
|
|
33
|
+
private readonly sentAt;
|
|
34
|
+
/** The path's own delay: the least a frame has taken to come back as shown. */
|
|
35
|
+
private floor;
|
|
36
|
+
private behindSince?;
|
|
37
|
+
private troubles;
|
|
38
|
+
private calmSince;
|
|
39
|
+
private calmFor;
|
|
40
|
+
private raisedAt?;
|
|
41
|
+
private firstSent?;
|
|
42
|
+
private heard;
|
|
43
|
+
private settledAt;
|
|
44
|
+
private lastSent;
|
|
45
|
+
constructor(now: number);
|
|
46
|
+
/** Whether frames are being dropped for the viewer to catch up. */
|
|
47
|
+
get dropping(): boolean;
|
|
48
|
+
/** How long ago the oldest frame not yet shown was sent. */
|
|
49
|
+
private lag;
|
|
50
|
+
/** The number to send this frame under, or undefined to drop it. */
|
|
51
|
+
next(key: boolean, now: number): number | undefined;
|
|
52
|
+
/** The viewer has shown everything up to `seq`. */
|
|
53
|
+
ack(seq: number, now: number): void;
|
|
54
|
+
/** Whether to move to a lighter picture ("down") or try a better one ("up"); asked after each frame. */
|
|
55
|
+
advice(now: number, canGoUp: boolean, canGoDown: boolean): "up" | "down" | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* The capture was started again (another level). The viewer drops what it had of the old stream
|
|
58
|
+
* without showing it, so none of that will ever be answered for: the new stream starts clean, with
|
|
59
|
+
* its keyframe, and is judged on its own frames once what was on the way has had time to arrive.
|
|
60
|
+
*/
|
|
61
|
+
restarted(now: number): void;
|
|
62
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Keeping a viewer close to live. The picture goes down pipes that deliver
|
|
2
|
+
// everything, in order (the gateway's relay, a data channel, a socket): when
|
|
3
|
+
// the network can't carry the stream, nothing is lost, it queues, and the
|
|
4
|
+
// phone ends up watching the past. So the viewer says which frame it has shown,
|
|
5
|
+
// and the host, knowing how far behind that is, stops adding to the queue
|
|
6
|
+
// (frames are dropped here, before anything relays them), resumes at a
|
|
7
|
+
// keyframe once the path has caught up, and moves to a lighter picture when
|
|
8
|
+
// that keeps happening.
|
|
9
|
+
/** How the picture is sent, best first. A gateway carries messages rather than video: relayed, it starts lower. */
|
|
10
|
+
export const LADDER = [
|
|
11
|
+
{ fps: 20, width: 1600, bitrate: "3M", ceiling: "4M" },
|
|
12
|
+
{ fps: 15, width: 1440, bitrate: "1500k", ceiling: "2M" },
|
|
13
|
+
{ fps: 12, width: 1280, bitrate: "900k", ceiling: "1300k" },
|
|
14
|
+
{ fps: 10, width: 1024, bitrate: "500k", ceiling: "700k" },
|
|
15
|
+
{ fps: 8, width: 854, bitrate: "260k", ceiling: "380k" },
|
|
16
|
+
];
|
|
17
|
+
/** The level a relayed stream starts at, and never goes above. */
|
|
18
|
+
export const RELAY_LEVEL = 2;
|
|
19
|
+
/** Behind by this much more than the path's own delay: stop sending. */
|
|
20
|
+
const BEHIND_MS = 450;
|
|
21
|
+
/** Caught up to within this of the path's own delay: resume (at a keyframe). */
|
|
22
|
+
const CAUGHT_UP_MS = 150;
|
|
23
|
+
/** Stuck behind for this long, or behind twice within TROUBLE_WINDOW_MS: a lighter picture. */
|
|
24
|
+
const STUCK_MS = 2500;
|
|
25
|
+
const TROUBLE_WINDOW_MS = 10_000;
|
|
26
|
+
/** Smooth for this long: try a better picture. Doubles each time the better one didn't hold. */
|
|
27
|
+
const CALM_MS = 30_000;
|
|
28
|
+
const CALM_MAX_MS = 240_000;
|
|
29
|
+
/** A viewer that never says what it has shown is not paced at all. */
|
|
30
|
+
const SILENT_MS = 5000;
|
|
31
|
+
/** After a change of picture, what was already on its way gets this long to arrive before more is concluded. */
|
|
32
|
+
const SETTLE_MS = 4000;
|
|
33
|
+
/** No path's own delay is taken to be longer than this: a stream that began on a choked path would look normal. */
|
|
34
|
+
const FLOOR_MAX_MS = 800;
|
|
35
|
+
/** Behind, and nothing heard for this long: a keyframe goes anyway. Whatever went wrong, the next answer ends it. */
|
|
36
|
+
const PROBE_MS = 8000;
|
|
37
|
+
export class Pacer {
|
|
38
|
+
seq = 0;
|
|
39
|
+
shown = 0;
|
|
40
|
+
sentAt = new Map();
|
|
41
|
+
/** The path's own delay: the least a frame has taken to come back as shown. */
|
|
42
|
+
floor = Infinity;
|
|
43
|
+
behindSince;
|
|
44
|
+
troubles = [];
|
|
45
|
+
calmSince;
|
|
46
|
+
calmFor = CALM_MS;
|
|
47
|
+
raisedAt;
|
|
48
|
+
firstSent;
|
|
49
|
+
heard = false;
|
|
50
|
+
settledAt = 0;
|
|
51
|
+
lastSent = 0;
|
|
52
|
+
constructor(now) {
|
|
53
|
+
this.calmSince = now;
|
|
54
|
+
}
|
|
55
|
+
/** Whether frames are being dropped for the viewer to catch up. */
|
|
56
|
+
get dropping() {
|
|
57
|
+
return this.behindSince !== undefined;
|
|
58
|
+
}
|
|
59
|
+
/** How long ago the oldest frame not yet shown was sent. */
|
|
60
|
+
lag(now) {
|
|
61
|
+
const oldest = this.sentAt.get(this.shown + 1);
|
|
62
|
+
return oldest === undefined ? 0 : now - oldest;
|
|
63
|
+
}
|
|
64
|
+
/** The number to send this frame under, or undefined to drop it. */
|
|
65
|
+
next(key, now) {
|
|
66
|
+
this.firstSent ??= now;
|
|
67
|
+
const paced = this.heard || now - this.firstSent < SILENT_MS;
|
|
68
|
+
if (paced) {
|
|
69
|
+
const own = Number.isFinite(this.floor) ? Math.min(this.floor, FLOOR_MAX_MS) : 0;
|
|
70
|
+
const lag = this.lag(now);
|
|
71
|
+
if (this.behindSince === undefined && lag > own + BEHIND_MS) {
|
|
72
|
+
this.behindSince = now;
|
|
73
|
+
this.troubles.push(now);
|
|
74
|
+
this.calmSince = now;
|
|
75
|
+
}
|
|
76
|
+
if (this.behindSince !== undefined) {
|
|
77
|
+
// Back in at a keyframe, once what was sent has been shown (or nearly).
|
|
78
|
+
const caughtUp = this.sentAt.size === 0 || lag <= own + CAUGHT_UP_MS;
|
|
79
|
+
if (!key || (!caughtUp && now - this.lastSent < PROBE_MS))
|
|
80
|
+
return undefined;
|
|
81
|
+
if (caughtUp) {
|
|
82
|
+
this.behindSince = undefined;
|
|
83
|
+
this.calmSince = now;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const seq = ++this.seq;
|
|
88
|
+
this.sentAt.set(seq, now);
|
|
89
|
+
this.lastSent = now;
|
|
90
|
+
// A viewer that says nothing must not make this grow for ever.
|
|
91
|
+
if (this.sentAt.size > 600)
|
|
92
|
+
this.sentAt.delete(this.sentAt.keys().next().value);
|
|
93
|
+
return seq;
|
|
94
|
+
}
|
|
95
|
+
/** The viewer has shown everything up to `seq`. */
|
|
96
|
+
ack(seq, now) {
|
|
97
|
+
if (!Number.isInteger(seq) || seq <= this.shown || seq > this.seq)
|
|
98
|
+
return;
|
|
99
|
+
this.heard = true;
|
|
100
|
+
const sent = this.sentAt.get(seq);
|
|
101
|
+
if (sent !== undefined)
|
|
102
|
+
this.floor = Math.min(this.floor, now - sent);
|
|
103
|
+
for (const key of this.sentAt.keys()) {
|
|
104
|
+
if (key > seq)
|
|
105
|
+
break;
|
|
106
|
+
this.sentAt.delete(key);
|
|
107
|
+
}
|
|
108
|
+
this.shown = seq;
|
|
109
|
+
}
|
|
110
|
+
/** Whether to move to a lighter picture ("down") or try a better one ("up"); asked after each frame. */
|
|
111
|
+
advice(now, canGoUp, canGoDown) {
|
|
112
|
+
if (now < this.settledAt)
|
|
113
|
+
return undefined;
|
|
114
|
+
this.troubles = this.troubles.filter((at) => at >= this.settledAt && now - at < TROUBLE_WINDOW_MS);
|
|
115
|
+
const stuck = this.behindSince !== undefined && now - Math.max(this.behindSince, this.settledAt) > STUCK_MS;
|
|
116
|
+
if (canGoDown && (stuck || this.troubles.length >= 2)) {
|
|
117
|
+
// The better picture just tried didn't hold: wait longer before trying it again.
|
|
118
|
+
if (this.raisedAt !== undefined && now - this.raisedAt < TROUBLE_WINDOW_MS * 2)
|
|
119
|
+
this.calmFor = Math.min(this.calmFor * 2, CALM_MAX_MS);
|
|
120
|
+
this.raisedAt = undefined;
|
|
121
|
+
return "down";
|
|
122
|
+
}
|
|
123
|
+
if (canGoUp && this.behindSince === undefined && now - this.calmSince > this.calmFor) {
|
|
124
|
+
this.raisedAt = now;
|
|
125
|
+
return "up";
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The capture was started again (another level). The viewer drops what it had of the old stream
|
|
131
|
+
* without showing it, so none of that will ever be answered for: the new stream starts clean, with
|
|
132
|
+
* its keyframe, and is judged on its own frames once what was on the way has had time to arrive.
|
|
133
|
+
*/
|
|
134
|
+
restarted(now) {
|
|
135
|
+
this.sentAt.clear();
|
|
136
|
+
this.shown = this.seq;
|
|
137
|
+
this.behindSince = undefined;
|
|
138
|
+
this.troubles = [];
|
|
139
|
+
this.calmSince = now;
|
|
140
|
+
this.settledAt = now + SETTLE_MS;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=screen-pacer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screen-pacer.js","sourceRoot":"","sources":["../../../../host/src/screen-pacer.ts"],"names":[],"mappings":"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"}
|
|
@@ -107,7 +107,18 @@ function sps(unit) {
|
|
|
107
107
|
if (unit[i] === 0 && unit[i + 1] === 0 && unit[i + 2] === 1 && (unit[i + 3] & 0x1f) === 7) return unit.subarray(i + 3);
|
|
108
108
|
}
|
|
109
109
|
if (!("VideoDecoder" in window)) say("这个系统版本的浏览器内核不支持视频解码,请升级系统后再试。");
|
|
110
|
-
let decoder;
|
|
110
|
+
let decoder, skipping = false, arrived = 0;
|
|
111
|
+
// The host is told which frame is on screen: that is how it knows when this end has fallen behind,
|
|
112
|
+
// and stops sending rather than let the picture drift into the past.
|
|
113
|
+
let lastShown = 0, telling = 0;
|
|
114
|
+
// For trying a slow path out without one: ?lag=2500&lagFor=12 answers 2.5 s late for the first 12 s.
|
|
115
|
+
const lag = Number(query.get("lag")) || 0, lagUntil = performance.now() + (Number(query.get("lagFor")) || 0) * 1000;
|
|
116
|
+
function shownUpTo(seq) {
|
|
117
|
+
if (seq > lastShown) lastShown = seq;
|
|
118
|
+
if (lag && performance.now() < lagUntil) { const late = lastShown; setTimeout(() => send({ t: "ack", n: late }), lag); return; }
|
|
119
|
+
if (!telling) telling = setTimeout(() => { telling = 0; send({ t: "ack", n: lastShown }); }, 40);
|
|
120
|
+
}
|
|
121
|
+
let lighterAt = -Infinity;
|
|
111
122
|
const ws = new WebSocket((location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/stream" + location.search);
|
|
112
123
|
ws.binaryType = "arraybuffer";
|
|
113
124
|
const send = (message) => { if (ws.readyState === 1) ws.send(JSON.stringify(message)); };
|
|
@@ -115,9 +126,15 @@ ws.onopen = () => { if (mode !== "view") askControl(); };
|
|
|
115
126
|
ws.onmessage = (event) => {
|
|
116
127
|
if (typeof event.data === "string") return heard(JSON.parse(event.data));
|
|
117
128
|
if (!("VideoDecoder" in window)) return;
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
const
|
|
129
|
+
// A frame: whether it is a keyframe, its number, the picture.
|
|
130
|
+
const head = new DataView(event.data);
|
|
131
|
+
const key = head.getUint8(0) === 1;
|
|
132
|
+
const seq = head.getUint32(1);
|
|
133
|
+
const unit = new Uint8Array(event.data, 5);
|
|
134
|
+
arrived = seq;
|
|
135
|
+
// A decoder that can't keep up skips to the next keyframe; what it skips has still arrived.
|
|
136
|
+
if (decoder && !key && (skipping || decoder.decodeQueueSize > 8)) { skipping = true; return shownUpTo(seq); }
|
|
137
|
+
skipping = false;
|
|
121
138
|
if (!decoder) {
|
|
122
139
|
const params = key && sps(unit);
|
|
123
140
|
if (!params) return;
|
|
@@ -129,6 +146,7 @@ ws.onmessage = (event) => {
|
|
|
129
146
|
layout();
|
|
130
147
|
}
|
|
131
148
|
ctx.drawImage(frame, 0, 0);
|
|
149
|
+
shownUpTo(frame.timestamp);
|
|
132
150
|
frame.close();
|
|
133
151
|
say("");
|
|
134
152
|
},
|
|
@@ -136,12 +154,24 @@ ws.onmessage = (event) => {
|
|
|
136
154
|
});
|
|
137
155
|
decoder.configure({ codec: "avc1." + hex(params[1]) + hex(params[2]) + hex(params[3]), optimizeForLatency: true });
|
|
138
156
|
}
|
|
139
|
-
decoder.decode(new EncodedVideoChunk({ type: key ? "key" : "delta", timestamp:
|
|
157
|
+
decoder.decode(new EncodedVideoChunk({ type: key ? "key" : "delta", timestamp: seq, data: unit }));
|
|
140
158
|
};
|
|
141
159
|
ws.onclose = () => { if (note.classList.contains("gone") || note.textContent.startsWith("正在")) say("屏幕连接已断开"); };
|
|
142
160
|
|
|
143
161
|
function heard(message) {
|
|
144
162
|
if (message.error) return say(message.error);
|
|
163
|
+
if (message.restart) {
|
|
164
|
+
// The host starts the stream again at another quality: a new decoder for it.
|
|
165
|
+
try { if (decoder) decoder.close(); } catch {}
|
|
166
|
+
decoder = undefined;
|
|
167
|
+
skipping = false;
|
|
168
|
+
// What the old decoder still held will never be shown: it has arrived, and that is said.
|
|
169
|
+
shownUpTo(arrived);
|
|
170
|
+
if (message.lighter && performance.now() - lighterAt > 30000) {
|
|
171
|
+
lighterAt = performance.now();
|
|
172
|
+
hint("网络较慢:已降低画质,优先保证跟得上", 3000);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
145
175
|
if (message.control) {
|
|
146
176
|
const was = controlling();
|
|
147
177
|
Object.assign(control, { known: true, reason: "" }, message.control);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screen-viewer.js","sourceRoot":"","sources":["../../../../host/src/screen-viewer.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,2EAA2E;AAC3E,6EAA6E;AAC7E,mEAAmE;AACnE,EAAE;AACF,4DAA4D;AAC5D,yEAAyE;AACzE,2CAA2C;AAC3C,6EAA6E;AAC7E,+EAA+E;AAC/E,2CAA2C;AAC3C,6FAA6F;AAE7F,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAC5B,uJAAuJ,IAAI,QAAQ,CAAC;AAEtK,MAAM,KAAK,GAAG;IACZ,IAAI,EAAE,IAAI,CAAC,qHAAqH,CAAC;IACjI,QAAQ,EAAE,IAAI,CAAC,6DAA6D,CAAC;IAC7E,KAAK,EAAE,IAAI,CAAC,oGAAoG,CAAC;IACjH,QAAQ,EAAE,IAAI,CAAC,iJAAiJ,CAAC;IACjK,GAAG,EAAE,IAAI,CAAC,0EAA0E,CAAC;IACrF,MAAM,EAAE,IAAI,CAAC,qHAAqH,CAAC;IACnI,MAAM,EAAE,IAAI,CAAC,oDAAoD,CAAC;IAClE,MAAM,EAAE,IAAI,CAAC,oDAAoD,CAAC;IAClE,IAAI,EAAE,IAAI,CAAC,4BAA4B,CAAC;CACzC,CAAC;AAEF,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCvB,CAAC;AAEF,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"screen-viewer.js","sourceRoot":"","sources":["../../../../host/src/screen-viewer.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,2EAA2E;AAC3E,6EAA6E;AAC7E,mEAAmE;AACnE,EAAE;AACF,4DAA4D;AAC5D,yEAAyE;AACzE,2CAA2C;AAC3C,6EAA6E;AAC7E,+EAA+E;AAC/E,2CAA2C;AAC3C,6FAA6F;AAE7F,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAC5B,uJAAuJ,IAAI,QAAQ,CAAC;AAEtK,MAAM,KAAK,GAAG;IACZ,IAAI,EAAE,IAAI,CAAC,qHAAqH,CAAC;IACjI,QAAQ,EAAE,IAAI,CAAC,6DAA6D,CAAC;IAC7E,KAAK,EAAE,IAAI,CAAC,oGAAoG,CAAC;IACjH,QAAQ,EAAE,IAAI,CAAC,iJAAiJ,CAAC;IACjK,GAAG,EAAE,IAAI,CAAC,0EAA0E,CAAC;IACrF,MAAM,EAAE,IAAI,CAAC,qHAAqH,CAAC;IACnI,MAAM,EAAE,IAAI,CAAC,oDAAoD,CAAC;IAClE,MAAM,EAAE,IAAI,CAAC,oDAAoD,CAAC;IAClE,IAAI,EAAE,IAAI,CAAC,4BAA4B,CAAC;CACzC,CAAC;AAEF,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCvB,CAAC;AAEF,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAisBxB,CAAC;AAEF,MAAM,UAAU,UAAU;IACxB,OAAO;;;;SAIA,KAAK;;;;;;;;;;;;kEAYoD,KAAK,CAAC,QAAQ;+DACjB,KAAK,CAAC,GAAG;gEACR,KAAK,CAAC,MAAM;;;;;;;;;;;;;;;;;;6CAkB/B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;UACxD,MAAM;eACD,CAAC;AAChB,CAAC"}
|
|
@@ -3,25 +3,6 @@ export interface Display {
|
|
|
3
3
|
index: number;
|
|
4
4
|
name: string;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
7
|
-
* How the picture is sent. `full` when it goes straight to the device; `low`
|
|
8
|
-
* when it is relayed by a gateway, which should carry messages rather than video.
|
|
9
|
-
*/
|
|
10
|
-
declare const PROFILES: {
|
|
11
|
-
readonly full: {
|
|
12
|
-
readonly fps: 20;
|
|
13
|
-
readonly width: 1600;
|
|
14
|
-
readonly bitrate: "3M";
|
|
15
|
-
readonly ceiling: "4M";
|
|
16
|
-
};
|
|
17
|
-
readonly low: {
|
|
18
|
-
readonly fps: 12;
|
|
19
|
-
readonly width: 1280;
|
|
20
|
-
readonly bitrate: "900k";
|
|
21
|
-
readonly ceiling: "1300k";
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
export type ScreenProfile = keyof typeof PROFILES;
|
|
25
6
|
export interface ScreenAccess {
|
|
26
7
|
supported: boolean;
|
|
27
8
|
ffmpeg: boolean;
|
|
@@ -30,7 +11,8 @@ export interface ScreenAccess {
|
|
|
30
11
|
app?: string;
|
|
31
12
|
problem?: string;
|
|
32
13
|
}
|
|
33
|
-
|
|
14
|
+
/** The capture for one rung of the ladder (see `screen-pacer.ts`): 0 is the best picture. */
|
|
15
|
+
export declare function captureArgs(display: number, level?: number): string[];
|
|
34
16
|
/** Splits an Annex-B stream at access-unit delimiters (NAL type 9) into whole frames. */
|
|
35
17
|
export declare class AccessUnitSplitter {
|
|
36
18
|
private buffer;
|
|
@@ -57,8 +39,13 @@ export declare class ScreenShare {
|
|
|
57
39
|
private token;
|
|
58
40
|
private displays;
|
|
59
41
|
private readonly captures;
|
|
60
|
-
/**
|
|
61
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The capture that has the screen. The system gives it to one at a time, and
|
|
44
|
+
* one started while another still holds it never gets a frame, even after
|
|
45
|
+
* the other has gone: the next starts once this one has.
|
|
46
|
+
*/
|
|
47
|
+
private holder?;
|
|
48
|
+
private turn;
|
|
62
49
|
private readonly controls;
|
|
63
50
|
constructor(log: (message: string) => void);
|
|
64
51
|
start(): Promise<{
|
|
@@ -74,9 +61,10 @@ export declare class ScreenShare {
|
|
|
74
61
|
* `ask`, the system asks for the first permission missing.
|
|
75
62
|
*/
|
|
76
63
|
access(ask: boolean): Promise<ScreenAccess>;
|
|
77
|
-
/** Starts a capture where it may record
|
|
64
|
+
/** Starts a capture where it may record. Undefined when it may not: the viewer has been told why. */
|
|
78
65
|
private capture;
|
|
66
|
+
/** Takes the screen for a new capture: whatever holds it is ended, and has left, before the new one starts. */
|
|
67
|
+
private take;
|
|
79
68
|
private stream;
|
|
80
69
|
stop(): void;
|
|
81
70
|
}
|
|
82
|
-
export {};
|
package/dist/host/src/screen.js
CHANGED
|
@@ -5,6 +5,7 @@ import { promisify } from "node:util";
|
|
|
5
5
|
import { WebSocketServer } from "ws";
|
|
6
6
|
import { RpcError } from "@linkshell/wire";
|
|
7
7
|
import { closeInputApp, hostAccess, inputApp, InputControl } from "./input.js";
|
|
8
|
+
import { LADDER, Pacer, RELAY_LEVEL } from "./screen-pacer.js";
|
|
8
9
|
import { viewerPage } from "./screen-viewer.js";
|
|
9
10
|
// Viewing the computer's screen from the phone. The host serves a small
|
|
10
11
|
// viewer page and an H.264 stream on a loopback port; the phone opens it
|
|
@@ -18,14 +19,6 @@ import { viewerPage } from "./screen-viewer.js";
|
|
|
18
19
|
// carries one, LinkShell.app, and the capture runs as its child: the
|
|
19
20
|
// permission is LinkShell's, whichever terminal started the host.
|
|
20
21
|
const run = promisify(execFile);
|
|
21
|
-
/**
|
|
22
|
-
* How the picture is sent. `full` when it goes straight to the device; `low`
|
|
23
|
-
* when it is relayed by a gateway, which should carry messages rather than video.
|
|
24
|
-
*/
|
|
25
|
-
const PROFILES = {
|
|
26
|
-
full: { fps: 20, width: 1600, bitrate: "3M", ceiling: "4M" },
|
|
27
|
-
low: { fps: 12, width: 1280, bitrate: "900k", ceiling: "1300k" },
|
|
28
|
-
};
|
|
29
22
|
async function hasFfmpeg() {
|
|
30
23
|
return run("ffmpeg", ["-hide_banner", "-version"], { timeout: 5000 }).then(() => true, () => false);
|
|
31
24
|
}
|
|
@@ -46,8 +39,9 @@ async function listDisplays() {
|
|
|
46
39
|
}
|
|
47
40
|
return displays;
|
|
48
41
|
}
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
/** The capture for one rung of the ladder (see `screen-pacer.ts`): 0 is the best picture. */
|
|
43
|
+
export function captureArgs(display, level = 0) {
|
|
44
|
+
const profile = LADDER[Math.min(Math.max(level, 0), LADDER.length - 1)];
|
|
51
45
|
const input = process.platform === "darwin"
|
|
52
46
|
? ["-f", "avfoundation", "-capture_cursor", "1", "-framerate", String(profile.fps), "-i", `${display}:none`]
|
|
53
47
|
: ["-f", "x11grab", "-framerate", String(profile.fps), "-i", process.env.DISPLAY ?? ":0"];
|
|
@@ -67,8 +61,9 @@ export function captureArgs(display, quality = "full") {
|
|
|
67
61
|
// ProMotion display): the frames are dropped here, before the encoder and the network pay for them.
|
|
68
62
|
`fps=${profile.fps},scale='min(${profile.width},iw)':-2,format=yuv420p`,
|
|
69
63
|
...encode,
|
|
64
|
+
// A keyframe a second: a viewer that fell behind is back on the live picture within one.
|
|
70
65
|
"-g",
|
|
71
|
-
String(profile.fps
|
|
66
|
+
String(profile.fps),
|
|
72
67
|
"-bf",
|
|
73
68
|
"0",
|
|
74
69
|
// An access-unit delimiter before every frame, so the stream splits into frames cleanly.
|
|
@@ -172,8 +167,13 @@ export class ScreenShare {
|
|
|
172
167
|
token = "";
|
|
173
168
|
displays = [];
|
|
174
169
|
captures = new Set();
|
|
175
|
-
/**
|
|
176
|
-
|
|
170
|
+
/**
|
|
171
|
+
* The capture that has the screen. The system gives it to one at a time, and
|
|
172
|
+
* one started while another still holds it never gets a frame, even after
|
|
173
|
+
* the other has gone: the next starts once this one has.
|
|
174
|
+
*/
|
|
175
|
+
holder;
|
|
176
|
+
turn = Promise.resolve();
|
|
177
177
|
controls = new Set();
|
|
178
178
|
constructor(log) {
|
|
179
179
|
this.log = log;
|
|
@@ -210,8 +210,9 @@ export class ScreenShare {
|
|
|
210
210
|
socket.destroy();
|
|
211
211
|
return;
|
|
212
212
|
}
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
// "low": the viewer is reached through a gateway, which should carry messages rather than video.
|
|
214
|
+
const relayed = url.searchParams.get("q") === "low";
|
|
215
|
+
sockets.handleUpgrade(request, socket, head, (ws) => this.stream(ws, Number(url.searchParams.get("display") ?? NaN), relayed));
|
|
215
216
|
});
|
|
216
217
|
this.server = server;
|
|
217
218
|
return new Promise((resolve, reject) => {
|
|
@@ -247,10 +248,20 @@ export class ScreenShare {
|
|
|
247
248
|
return { supported, ffmpeg, recording: null, control: null, problem: error.message };
|
|
248
249
|
}
|
|
249
250
|
}
|
|
250
|
-
/** Starts a capture where it may record
|
|
251
|
-
async capture(index,
|
|
252
|
-
const args = captureArgs(index,
|
|
251
|
+
/** Starts a capture where it may record. Undefined when it may not: the viewer has been told why. */
|
|
252
|
+
async capture(index, level, on) {
|
|
253
|
+
const args = captureArgs(index, level);
|
|
254
|
+
let ended = false;
|
|
255
|
+
let leave = () => { };
|
|
256
|
+
const gone = new Promise((resolve) => (leave = resolve));
|
|
257
|
+
const data = (chunk) => {
|
|
258
|
+
if (!ended)
|
|
259
|
+
on.data(chunk);
|
|
260
|
+
};
|
|
253
261
|
const finished = (code, errors) => {
|
|
262
|
+
leave();
|
|
263
|
+
if (ended)
|
|
264
|
+
return;
|
|
254
265
|
if (code)
|
|
255
266
|
this.log(`[screen] capture exited ${code}: ${errors.trim()}`);
|
|
256
267
|
on.exit(!code ? undefined : /permission|not authorized|denied/i.test(errors) ? notAllowed("") : "屏幕捕获失败");
|
|
@@ -262,13 +273,16 @@ export class ScreenShare {
|
|
|
262
273
|
// Someone may be at the computer: the system's question, and its settings, come up there.
|
|
263
274
|
void app.ask("recording").catch(() => { });
|
|
264
275
|
on.exit(notAllowed(status.app));
|
|
265
|
-
return
|
|
276
|
+
return undefined;
|
|
266
277
|
}
|
|
267
|
-
const stop = await app.capture(await ffmpegPath(), args, { data
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
278
|
+
const stop = await app.capture(await ffmpegPath(), args, { data, exit: finished });
|
|
279
|
+
return {
|
|
280
|
+
on,
|
|
281
|
+
gone,
|
|
282
|
+
end: () => {
|
|
283
|
+
ended = true;
|
|
284
|
+
stop();
|
|
285
|
+
},
|
|
272
286
|
};
|
|
273
287
|
}
|
|
274
288
|
if (process.platform === "darwin") {
|
|
@@ -277,22 +291,50 @@ export class ScreenShare {
|
|
|
277
291
|
if (status && !status.recording) {
|
|
278
292
|
void hostAccess(this.log, "recording").catch(() => { });
|
|
279
293
|
on.exit(notAllowed(status.app));
|
|
280
|
-
return
|
|
294
|
+
return undefined;
|
|
281
295
|
}
|
|
282
296
|
}
|
|
283
297
|
const capture = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
284
298
|
this.captures.add(capture);
|
|
285
299
|
let errors = "";
|
|
286
|
-
capture.stdout.on("data",
|
|
300
|
+
capture.stdout.on("data", data);
|
|
287
301
|
capture.stderr.on("data", (chunk) => (errors = (errors + chunk.toString()).slice(-2000)));
|
|
288
302
|
capture.on("error", (error) => finished(-1, error.message));
|
|
289
303
|
capture.on("exit", (code) => {
|
|
290
304
|
this.captures.delete(capture);
|
|
291
305
|
finished(code ?? 0, errors);
|
|
292
306
|
});
|
|
293
|
-
return
|
|
307
|
+
return {
|
|
308
|
+
on,
|
|
309
|
+
gone,
|
|
310
|
+
end: () => {
|
|
311
|
+
ended = true;
|
|
312
|
+
endCapture(capture);
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/** Takes the screen for a new capture: whatever holds it is ended, and has left, before the new one starts. */
|
|
317
|
+
take(index, level, on, wanted) {
|
|
318
|
+
const mine = this.turn.then(async () => {
|
|
319
|
+
const previous = this.holder;
|
|
320
|
+
if (previous) {
|
|
321
|
+
this.holder = undefined;
|
|
322
|
+
previous.end();
|
|
323
|
+
// Another device's picture ends here; a viewer changing its own just carries on.
|
|
324
|
+
if (previous.on !== on)
|
|
325
|
+
previous.on.exit("屏幕画面被另一台设备接手了");
|
|
326
|
+
await Promise.race([previous.gone, new Promise((resolve) => setTimeout(resolve, 3000))]);
|
|
327
|
+
}
|
|
328
|
+
if (!wanted())
|
|
329
|
+
return undefined;
|
|
330
|
+
const running = await this.capture(index, level, on);
|
|
331
|
+
this.holder = running;
|
|
332
|
+
return running;
|
|
333
|
+
});
|
|
334
|
+
this.turn = mine.catch(() => { });
|
|
335
|
+
return mine;
|
|
294
336
|
}
|
|
295
|
-
stream(ws, display,
|
|
337
|
+
stream(ws, display, relayed) {
|
|
296
338
|
const shown = this.displays.find((entry) => entry.index === display) ?? this.displays[0];
|
|
297
339
|
const tell = (message) => ws.readyState === ws.OPEN && ws.send(JSON.stringify(message));
|
|
298
340
|
// The viewer's hands: started when it first asks to control, for the display it is watching.
|
|
@@ -302,63 +344,95 @@ export class ScreenShare {
|
|
|
302
344
|
posted: (event) => this.log(`[screen] dry run: ${JSON.stringify(event)}`),
|
|
303
345
|
}, this.log);
|
|
304
346
|
this.controls.add(control);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
message = JSON.parse(String(data));
|
|
311
|
-
}
|
|
312
|
-
catch {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
if (message?.t === "control")
|
|
316
|
-
void control.start(shown?.screen ?? 0);
|
|
317
|
-
else
|
|
318
|
-
control.send(message);
|
|
319
|
-
});
|
|
320
|
-
const splitter = new AccessUnitSplitter();
|
|
347
|
+
// The picture: as good as the path carries without falling behind (see `screen-pacer.ts`).
|
|
348
|
+
const best = relayed ? RELAY_LEVEL : 0;
|
|
349
|
+
const pacer = new Pacer(Date.now());
|
|
350
|
+
let level = best;
|
|
351
|
+
let splitter = new AccessUnitSplitter();
|
|
321
352
|
let closed = false;
|
|
322
|
-
let
|
|
323
|
-
|
|
353
|
+
let changing = false;
|
|
354
|
+
let running;
|
|
355
|
+
const events = {
|
|
324
356
|
data: (chunk) => splitter.push(chunk, (unit, key) => {
|
|
325
357
|
if (ws.readyState !== ws.OPEN)
|
|
326
358
|
return;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
359
|
+
const now = Date.now();
|
|
360
|
+
const seq = pacer.next(key, now);
|
|
361
|
+
if (seq !== undefined) {
|
|
362
|
+
const head = Buffer.allocUnsafe(5);
|
|
363
|
+
head[0] = key ? 1 : 0;
|
|
364
|
+
head.writeUInt32BE(seq >>> 0, 1);
|
|
365
|
+
ws.send(Buffer.concat([head, unit]));
|
|
366
|
+
}
|
|
367
|
+
const advice = changing ? undefined : pacer.advice(now, level > best, level < LADDER.length - 1);
|
|
368
|
+
if (advice)
|
|
369
|
+
void change(level + (advice === "down" ? 1 : -1));
|
|
331
370
|
}),
|
|
332
371
|
exit: (message) => {
|
|
333
372
|
if (message)
|
|
334
373
|
tell({ error: message });
|
|
335
374
|
ws.close();
|
|
336
375
|
},
|
|
337
|
-
}
|
|
376
|
+
};
|
|
377
|
+
const begin = async () => {
|
|
378
|
+
const started = await this.take(shown?.index ?? 0, level, events, () => !closed);
|
|
338
379
|
if (closed)
|
|
339
|
-
end();
|
|
380
|
+
started?.end();
|
|
340
381
|
else
|
|
341
|
-
|
|
342
|
-
}
|
|
382
|
+
running = started;
|
|
383
|
+
};
|
|
384
|
+
const failed = (error) => {
|
|
343
385
|
this.log(`[screen] the capture did not start: ${error.message}`);
|
|
344
386
|
tell({ error: "屏幕捕获失败" });
|
|
345
387
|
ws.close();
|
|
388
|
+
};
|
|
389
|
+
const change = async (next) => {
|
|
390
|
+
changing = true;
|
|
391
|
+
this.log(`[screen] picture ${next > level ? "lighter" : "better"}: ${LADDER[next].width} wide, ${LADDER[next].fps} a second, ${LADDER[next].bitrate}`);
|
|
392
|
+
// Nothing more of the old stream goes out; the page starts its decoder afresh for the new one.
|
|
393
|
+
running?.end();
|
|
394
|
+
tell({ restart: true, level: next, lighter: next > level });
|
|
395
|
+
level = next;
|
|
396
|
+
splitter = new AccessUnitSplitter();
|
|
397
|
+
await begin().catch(failed);
|
|
398
|
+
pacer.restarted(Date.now());
|
|
399
|
+
changing = false;
|
|
400
|
+
};
|
|
401
|
+
void begin().catch(failed);
|
|
402
|
+
ws.on("message", (data, binary) => {
|
|
403
|
+
if (binary)
|
|
404
|
+
return;
|
|
405
|
+
let message;
|
|
406
|
+
try {
|
|
407
|
+
message = JSON.parse(String(data));
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const kind = message?.t;
|
|
413
|
+
if (kind === "ack")
|
|
414
|
+
pacer.ack(Number(message.n), Date.now());
|
|
415
|
+
else if (kind === "control")
|
|
416
|
+
void control.start(shown?.screen ?? 0);
|
|
417
|
+
else
|
|
418
|
+
control.send(message);
|
|
346
419
|
});
|
|
347
420
|
ws.on("close", () => {
|
|
348
421
|
closed = true;
|
|
349
|
-
|
|
422
|
+
if (running && this.holder === running)
|
|
423
|
+
this.holder = undefined;
|
|
424
|
+
running?.end();
|
|
350
425
|
control.stop();
|
|
351
426
|
this.controls.delete(control);
|
|
352
427
|
});
|
|
353
428
|
}
|
|
354
429
|
stop() {
|
|
355
430
|
// The host is going: there is no later to insist in.
|
|
431
|
+
this.holder?.end();
|
|
432
|
+
this.holder = undefined;
|
|
356
433
|
for (const capture of this.captures)
|
|
357
434
|
endCapture(capture, true);
|
|
358
435
|
this.captures.clear();
|
|
359
|
-
for (const stop of this.stops)
|
|
360
|
-
stop();
|
|
361
|
-
this.stops.clear();
|
|
362
436
|
for (const control of this.controls)
|
|
363
437
|
control.stop();
|
|
364
438
|
this.controls.clear();
|