linkshell-cli 0.9.0 → 0.10.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/README.md +11 -6
- package/dist/cli/src/commands/gateway.d.ts +2 -0
- package/dist/cli/src/commands/gateway.js +29 -0
- package/dist/cli/src/commands/gateway.js.map +1 -0
- package/dist/cli/src/commands/host.d.ts +2 -0
- package/dist/cli/src/commands/host.js +12 -2
- package/dist/cli/src/commands/host.js.map +1 -1
- package/dist/cli/src/commands/setup.js +4 -2
- package/dist/cli/src/commands/setup.js.map +1 -1
- package/dist/cli/src/index.js +12 -334
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/utils/daemon.d.ts +0 -7
- package/dist/cli/src/utils/daemon.js +1 -18
- package/dist/cli/src/utils/daemon.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/gateway/src/accounts.d.ts +6 -0
- package/dist/gateway/src/accounts.js +32 -0
- package/dist/gateway/src/accounts.js.map +1 -0
- package/dist/gateway/src/index.d.ts +4 -0
- package/dist/gateway/src/index.js +5 -0
- package/dist/gateway/src/index.js.map +1 -0
- package/dist/gateway/src/main.d.ts +1 -0
- package/dist/gateway/src/main.js +57 -0
- package/dist/gateway/src/main.js.map +1 -0
- package/dist/gateway/src/rate-limit.d.ts +22 -0
- package/dist/gateway/src/rate-limit.js +66 -0
- package/dist/gateway/src/rate-limit.js.map +1 -0
- package/dist/gateway/src/relay.d.ts +84 -0
- package/dist/gateway/src/relay.js +434 -0
- package/dist/gateway/src/relay.js.map +1 -0
- package/dist/gateway/src/serve.d.ts +34 -0
- package/dist/gateway/src/serve.js +102 -0
- package/dist/gateway/src/serve.js.map +1 -0
- package/dist/gateway/src/store.d.ts +35 -0
- package/dist/gateway/src/store.js +109 -0
- package/dist/gateway/src/store.js.map +1 -0
- package/dist/gateway/src/subscription.d.ts +18 -0
- package/dist/gateway/src/subscription.js +46 -0
- package/dist/gateway/src/subscription.js.map +1 -0
- package/dist/host/src/drivers/claude/transcript.d.ts +23 -23
- package/dist/host/src/fs.d.ts +3 -3
- package/dist/host/src/input.d.ts +4 -4
- package/dist/host/src/terminals.d.ts +1 -1
- package/dist/wire/src/model.d.ts +6 -6
- package/dist/wire/src/relay.d.ts +8 -8
- package/dist/wire/src/rpc.d.ts +818 -818
- package/dist/wire/src/updates.d.ts +18 -18
- package/package.json +4 -13
- package/src/commands/gateway.ts +31 -0
- package/src/commands/host.ts +13 -2
- package/src/commands/setup.ts +4 -2
- package/src/index.ts +14 -429
- package/src/utils/daemon.ts +2 -27
- package/dist/cli/src/config.d.ts +0 -14
- package/dist/cli/src/config.js +0 -23
- package/dist/cli/src/config.js.map +0 -1
- package/dist/cli/src/machine-id.d.ts +0 -6
- package/dist/cli/src/machine-id.js +0 -50
- package/dist/cli/src/machine-id.js.map +0 -1
- package/dist/cli/src/providers.d.ts +0 -13
- package/dist/cli/src/providers.js +0 -77
- package/dist/cli/src/providers.js.map +0 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -128
- package/dist/cli/src/runtime/acp/acp-client.js +0 -430
- package/dist/cli/src/runtime/acp/acp-client.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-process-discovery.d.ts +0 -48
- package/dist/cli/src/runtime/acp/agent-process-discovery.js +0 -278
- package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-session.d.ts +0 -62
- package/dist/cli/src/runtime/acp/agent-session.js +0 -1081
- package/dist/cli/src/runtime/acp/agent-session.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +0 -311
- package/dist/cli/src/runtime/acp/agent-workspace.js +0 -6083
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +0 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +0 -121
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +0 -1147
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +0 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +0 -53
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +0 -552
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +0 -1
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +0 -22
- package/dist/cli/src/runtime/acp/json-rpc.js +0 -160
- package/dist/cli/src/runtime/acp/json-rpc.js.map +0 -1
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -16
- package/dist/cli/src/runtime/acp/provider-resolver.js +0 -132
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +0 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +0 -97
- package/dist/cli/src/runtime/bridge-session.js +0 -1364
- package/dist/cli/src/runtime/bridge-session.js.map +0 -1
- package/dist/cli/src/runtime/hook-command.d.ts +0 -9
- package/dist/cli/src/runtime/hook-command.js +0 -80
- package/dist/cli/src/runtime/hook-command.js.map +0 -1
- package/dist/cli/src/runtime/screen-fallback.d.ts +0 -22
- package/dist/cli/src/runtime/screen-fallback.js +0 -202
- package/dist/cli/src/runtime/screen-fallback.js.map +0 -1
- package/dist/cli/src/runtime/screen-share.d.ts +0 -34
- package/dist/cli/src/runtime/screen-share.js +0 -376
- package/dist/cli/src/runtime/screen-share.js.map +0 -1
- package/dist/cli/src/runtime/scrollback.d.ts +0 -11
- package/dist/cli/src/runtime/scrollback.js +0 -33
- package/dist/cli/src/runtime/scrollback.js.map +0 -1
- package/dist/cli/src/runtime/usage-report.d.ts +0 -34
- package/dist/cli/src/runtime/usage-report.js +0 -332
- package/dist/cli/src/runtime/usage-report.js.map +0 -1
- package/dist/cli/src/utils/keep-awake.d.ts +0 -6
- package/dist/cli/src/utils/keep-awake.js +0 -48
- package/dist/cli/src/utils/keep-awake.js.map +0 -1
- package/dist/cli/src/utils/lan-ip.d.ts +0 -9
- package/dist/cli/src/utils/lan-ip.js +0 -61
- package/dist/cli/src/utils/lan-ip.js.map +0 -1
- package/dist/host/src/input-helper.d.ts +0 -1
- package/dist/host/src/input-helper.js +0 -564
- package/dist/host/src/input-helper.js.map +0 -1
- package/dist/shared-protocol/src/index.d.ts +0 -17029
- package/dist/shared-protocol/src/index.js +0 -1064
- package/dist/shared-protocol/src/index.js.map +0 -1
- package/scripts/copy-web.mjs +0 -34
- package/src/config.ts +0 -36
- package/src/machine-id.ts +0 -61
- package/src/providers.ts +0 -97
- package/src/runtime/acp/acp-client.ts +0 -521
- package/src/runtime/acp/agent-process-discovery.ts +0 -308
- package/src/runtime/acp/agent-session.ts +0 -1186
- package/src/runtime/acp/agent-workspace.ts +0 -6590
- package/src/runtime/acp/claude-sdk-client.ts +0 -1221
- package/src/runtime/acp/claude-stream-json-client.ts +0 -633
- package/src/runtime/acp/json-rpc.ts +0 -184
- package/src/runtime/acp/provider-resolver.ts +0 -154
- package/src/runtime/bridge-session.ts +0 -1549
- package/src/runtime/hook-command.ts +0 -73
- package/src/runtime/screen-fallback.ts +0 -235
- package/src/runtime/screen-share.ts +0 -426
- package/src/runtime/scrollback.ts +0 -43
- package/src/runtime/usage-report.ts +0 -342
- package/src/utils/keep-awake.ts +0 -61
- package/src/utils/lan-ip.ts +0 -61
- package/web/assets/FileBrowser-wOAfY-xj.js +0 -2
- package/web/assets/PortPreview-Cbvv7yzA.js +0 -2
- package/web/assets/TerminalPanel-CTSklkct.js +0 -1
- package/web/assets/UsageDashboard-CJ1AsNmd.js +0 -1
- package/web/assets/index-BsnDaeCt.css +0 -33
- package/web/assets/index-q85psYZ5.js +0 -36
- package/web/assets/vendor-highlight-NXWAAyIv.js +0 -5
- package/web/assets/vendor-react-M7xG3qiw.js +0 -49
- package/web/assets/vendor-xterm-D15THcI4.js +0 -9
- package/web/index.html +0 -37
- package/web/logos/claude.png +0 -0
- package/web/logos/codex.png +0 -0
- package/web/logos/copilot.png +0 -0
- package/web/logos/gemini.png +0 -0
- package/web/logos/linkshell-1024.png +0 -0
- package/web/logos/linkshell-splash.png +0 -0
- package/web/logos/linkshell.png +0 -0
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
import { spawn, execSync } from "node:child_process";
|
|
2
|
-
import type { ChildProcess } from "node:child_process";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
import { platform } from "node:os";
|
|
5
|
-
import { createEnvelope } from "@linkshell/protocol";
|
|
6
|
-
import type { Envelope } from "@linkshell/protocol";
|
|
7
|
-
|
|
8
|
-
const RTP_MTU = 1200; // Max payload size before FU-A fragmentation
|
|
9
|
-
|
|
10
|
-
/** Detect the avfoundation device index for "Capture screen" on macOS. */
|
|
11
|
-
function detectMacScreenIndex(): string {
|
|
12
|
-
try {
|
|
13
|
-
const out = execSync('ffmpeg -f avfoundation -list_devices true -i "" 2>&1 || true', {
|
|
14
|
-
timeout: 5000,
|
|
15
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
16
|
-
}).toString();
|
|
17
|
-
// Look for lines like: [AVFoundation ...] [2] Capture screen 0
|
|
18
|
-
const match = out.match(/\[(\d+)]\s+Capture screen/);
|
|
19
|
-
if (match) return match[1]!;
|
|
20
|
-
} catch {}
|
|
21
|
-
return "1"; // legacy fallback
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ScreenShareOptions {
|
|
25
|
-
sessionId: string;
|
|
26
|
-
fps: number;
|
|
27
|
-
quality: number;
|
|
28
|
-
scale: number;
|
|
29
|
-
turnUrl?: string;
|
|
30
|
-
turnUser?: string;
|
|
31
|
-
turnPass?: string;
|
|
32
|
-
onSignal: (envelope: Envelope) => void;
|
|
33
|
-
onStatus: (envelope: Envelope) => void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* WebRTC screen sharing using werift (pure TS WebRTC) + ffmpeg.
|
|
38
|
-
*
|
|
39
|
-
* Flow:
|
|
40
|
-
* 1. ffmpeg captures screen → H.264 → writes to stdout as raw annexb
|
|
41
|
-
* 2. We read H.264 NAL units and push them into werift video track
|
|
42
|
-
* 3. werift handles ICE/DTLS/SRTP and sends to remote peer
|
|
43
|
-
* 4. Signaling (SDP offer/answer, ICE candidates) goes through existing WebSocket
|
|
44
|
-
*/
|
|
45
|
-
export class ScreenShare {
|
|
46
|
-
private pc: any; // werift.RTCPeerConnection
|
|
47
|
-
private ffmpeg: ChildProcess | undefined;
|
|
48
|
-
private active = false;
|
|
49
|
-
private readonly options: ScreenShareOptions;
|
|
50
|
-
|
|
51
|
-
constructor(options: ScreenShareOptions) {
|
|
52
|
-
this.options = options;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
static isAvailable(): boolean {
|
|
56
|
-
// Check ffmpeg exists
|
|
57
|
-
try {
|
|
58
|
-
execSync("which ffmpeg", { stdio: "pipe" });
|
|
59
|
-
} catch {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
// Check werift can be resolved from this package's context
|
|
63
|
-
try {
|
|
64
|
-
const require = createRequire(import.meta.url);
|
|
65
|
-
require.resolve("werift");
|
|
66
|
-
const werift = require("werift") as any;
|
|
67
|
-
if (!werift.RTCPeerConnection || !werift.MediaStreamTrack) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
return true;
|
|
71
|
-
} catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async start(): Promise<void> {
|
|
77
|
-
if (this.active) return;
|
|
78
|
-
this.active = true;
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
const werift = await import("werift") as any;
|
|
82
|
-
|
|
83
|
-
// ICE servers
|
|
84
|
-
const iceServers: any[] = [
|
|
85
|
-
{ urls: "stun:stun.l.google.com:19302" },
|
|
86
|
-
];
|
|
87
|
-
if (this.options.turnUrl) {
|
|
88
|
-
iceServers.push({
|
|
89
|
-
urls: this.options.turnUrl,
|
|
90
|
-
username: this.options.turnUser ?? "",
|
|
91
|
-
credential: this.options.turnPass ?? "",
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this.pc = new werift.RTCPeerConnection({
|
|
96
|
-
iceServers,
|
|
97
|
-
bundlePolicy: "max-bundle",
|
|
98
|
-
codecs: { video: [werift.useH264()] },
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// Create video track — werift defaults to H264, no need to set codecs explicitly
|
|
102
|
-
// (setting transceiver.codecs crashes createOffer in werift 0.22.x)
|
|
103
|
-
const videoTrack = new werift.MediaStreamTrack({ kind: "video" });
|
|
104
|
-
this.pc.addTransceiver(videoTrack, {
|
|
105
|
-
direction: "sendonly",
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// ICE candidate → send to remote
|
|
109
|
-
this.pc.onIceCandidate.subscribe((candidate: any) => {
|
|
110
|
-
if (candidate) {
|
|
111
|
-
this.options.onSignal(
|
|
112
|
-
createEnvelope({
|
|
113
|
-
type: "screen.ice",
|
|
114
|
-
sessionId: this.options.sessionId,
|
|
115
|
-
payload: {
|
|
116
|
-
candidate: candidate.candidate,
|
|
117
|
-
sdpMid: candidate.sdpMid ?? null,
|
|
118
|
-
sdpMLineIndex: candidate.sdpMLineIndex ?? null,
|
|
119
|
-
},
|
|
120
|
-
}),
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Connection state changes
|
|
126
|
-
this.pc.connectionStateChange.subscribe((state: string) => {
|
|
127
|
-
if (state === "connected") {
|
|
128
|
-
this.options.onStatus(
|
|
129
|
-
createEnvelope({
|
|
130
|
-
type: "screen.status",
|
|
131
|
-
sessionId: this.options.sessionId,
|
|
132
|
-
payload: { active: true, mode: "webrtc" as const },
|
|
133
|
-
}),
|
|
134
|
-
);
|
|
135
|
-
} else if (state === "failed" || state === "disconnected" || state === "closed") {
|
|
136
|
-
this.options.onStatus(
|
|
137
|
-
createEnvelope({
|
|
138
|
-
type: "screen.status",
|
|
139
|
-
sessionId: this.options.sessionId,
|
|
140
|
-
payload: { active: false, mode: "off" as const, error: `WebRTC ${state}` },
|
|
141
|
-
}),
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Create offer
|
|
147
|
-
const offer = await this.pc.createOffer();
|
|
148
|
-
await this.pc.setLocalDescription(offer);
|
|
149
|
-
|
|
150
|
-
// Notify client that WebRTC mode is active BEFORE sending the offer,
|
|
151
|
-
// so the client mounts the WebView and is ready to process the offer.
|
|
152
|
-
this.options.onStatus(
|
|
153
|
-
createEnvelope({
|
|
154
|
-
type: "screen.status",
|
|
155
|
-
sessionId: this.options.sessionId,
|
|
156
|
-
payload: { active: true, mode: "webrtc" as const },
|
|
157
|
-
}),
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
this.options.onSignal(
|
|
161
|
-
createEnvelope({
|
|
162
|
-
type: "screen.offer",
|
|
163
|
-
sessionId: this.options.sessionId,
|
|
164
|
-
payload: { sdp: offer.sdp },
|
|
165
|
-
}),
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
// Start ffmpeg capture (will begin sending once ICE connects)
|
|
169
|
-
this.startFfmpeg(videoTrack, werift);
|
|
170
|
-
|
|
171
|
-
} catch (err) {
|
|
172
|
-
this.options.onStatus(
|
|
173
|
-
createEnvelope({
|
|
174
|
-
type: "screen.status",
|
|
175
|
-
sessionId: this.options.sessionId,
|
|
176
|
-
payload: {
|
|
177
|
-
active: false,
|
|
178
|
-
mode: "off" as const,
|
|
179
|
-
error: `WebRTC init failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
180
|
-
},
|
|
181
|
-
}),
|
|
182
|
-
);
|
|
183
|
-
this.active = false;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async handleAnswer(sdp: string): Promise<void> {
|
|
188
|
-
if (!this.pc) return;
|
|
189
|
-
try {
|
|
190
|
-
const werift = await import("werift") as any;
|
|
191
|
-
await this.pc.setRemoteDescription(
|
|
192
|
-
new werift.RTCSessionDescription(sdp, "answer"),
|
|
193
|
-
);
|
|
194
|
-
} catch (err) {
|
|
195
|
-
process.stderr.write(`[screen-share] failed to set answer: ${err}\n`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async handleIceCandidate(candidate: string, sdpMid?: string | null, sdpMLineIndex?: number | null): Promise<void> {
|
|
200
|
-
if (!this.pc) return;
|
|
201
|
-
try {
|
|
202
|
-
const werift = await import("werift") as any;
|
|
203
|
-
await this.pc.addIceCandidate(
|
|
204
|
-
new werift.RTCIceCandidate({ candidate, sdpMid: sdpMid ?? undefined, sdpMLineIndex: sdpMLineIndex ?? undefined }),
|
|
205
|
-
);
|
|
206
|
-
} catch (err) {
|
|
207
|
-
process.stderr.write(`[screen-share] failed to add ICE candidate: ${err}\n`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
stop(): void {
|
|
212
|
-
this.active = false;
|
|
213
|
-
if (this.ffmpeg) {
|
|
214
|
-
this.ffmpeg.kill("SIGTERM");
|
|
215
|
-
this.ffmpeg = undefined;
|
|
216
|
-
}
|
|
217
|
-
if (this.pc) {
|
|
218
|
-
try { this.pc.close(); } catch {}
|
|
219
|
-
this.pc = undefined;
|
|
220
|
-
}
|
|
221
|
-
this.options.onStatus(
|
|
222
|
-
createEnvelope({
|
|
223
|
-
type: "screen.status",
|
|
224
|
-
sessionId: this.options.sessionId,
|
|
225
|
-
payload: { active: false, mode: "off" as const },
|
|
226
|
-
}),
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
private startFfmpeg(videoTrack: any, werift: any): void {
|
|
231
|
-
const os = platform();
|
|
232
|
-
const fps = this.options.fps;
|
|
233
|
-
const scale = this.options.scale;
|
|
234
|
-
|
|
235
|
-
let inputArgs: string[];
|
|
236
|
-
if (os === "darwin") {
|
|
237
|
-
const screenIdx = detectMacScreenIndex();
|
|
238
|
-
inputArgs = ["-f", "avfoundation", "-framerate", String(fps), "-i", `${screenIdx}:none`];
|
|
239
|
-
} else if (os === "linux") {
|
|
240
|
-
inputArgs = ["-f", "x11grab", "-framerate", String(fps), "-i", ":0"];
|
|
241
|
-
} else {
|
|
242
|
-
inputArgs = ["-f", "gdigrab", "-framerate", String(fps), "-i", "desktop"];
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// libx264 requires even width & height; crop 1px if needed
|
|
246
|
-
const evenFilter = "crop=trunc(iw/2)*2:trunc(ih/2)*2";
|
|
247
|
-
const vf = scale < 1
|
|
248
|
-
? ["-vf", `scale=iw*${scale}:ih*${scale},${evenFilter}`]
|
|
249
|
-
: ["-vf", evenFilter];
|
|
250
|
-
|
|
251
|
-
this.ffmpeg = spawn("ffmpeg", [
|
|
252
|
-
...inputArgs,
|
|
253
|
-
...vf,
|
|
254
|
-
"-c:v", "libx264",
|
|
255
|
-
"-preset", "ultrafast",
|
|
256
|
-
"-tune", "zerolatency",
|
|
257
|
-
"-profile:v", "baseline",
|
|
258
|
-
"-level", "3.1",
|
|
259
|
-
"-pix_fmt", "yuv420p",
|
|
260
|
-
"-g", String(fps * 2),
|
|
261
|
-
"-b:v", "1500k",
|
|
262
|
-
"-maxrate", "2000k",
|
|
263
|
-
"-bufsize", "4000k",
|
|
264
|
-
"-f", "h264",
|
|
265
|
-
"-an",
|
|
266
|
-
"pipe:1",
|
|
267
|
-
], {
|
|
268
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// RTP state
|
|
272
|
-
let seqNum = 0;
|
|
273
|
-
let timestamp = -Math.floor(90000 / fps); // so first bump yields 0
|
|
274
|
-
const clockRate = 90000;
|
|
275
|
-
const frameDuration = Math.floor(clockRate / fps);
|
|
276
|
-
const ssrc = (Math.random() * 0xffffffff) >>> 0;
|
|
277
|
-
|
|
278
|
-
// Parse NAL units from annexb stream and packetize as RTP
|
|
279
|
-
let nalBuffer = Buffer.alloc(0);
|
|
280
|
-
|
|
281
|
-
const sendNalUnit = (nal: Buffer) => {
|
|
282
|
-
if (nal.length === 0) return;
|
|
283
|
-
|
|
284
|
-
// Strip start code
|
|
285
|
-
let offset = 0;
|
|
286
|
-
if (nal[0] === 0 && nal[1] === 0 && nal[2] === 0 && nal[3] === 1) {
|
|
287
|
-
offset = 4;
|
|
288
|
-
} else if (nal[0] === 0 && nal[1] === 0 && nal[2] === 1) {
|
|
289
|
-
offset = 3;
|
|
290
|
-
}
|
|
291
|
-
const nalData = nal.subarray(offset);
|
|
292
|
-
if (nalData.length === 0) return;
|
|
293
|
-
|
|
294
|
-
const nalType = nalData[0]! & 0x1f;
|
|
295
|
-
// Update timestamp on each new access unit (SPS/IDR/non-IDR slice)
|
|
296
|
-
if (nalType === 7 || nalType === 5 || nalType === 1) {
|
|
297
|
-
// Only bump timestamp for actual picture NALs (not SPS/PPS)
|
|
298
|
-
if (nalType === 5 || nalType === 1) {
|
|
299
|
-
timestamp += frameDuration;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (nalData.length <= RTP_MTU) {
|
|
304
|
-
// Single NAL unit packet
|
|
305
|
-
const header = new werift.RtpHeader({
|
|
306
|
-
sequenceNumber: seqNum++ & 0xffff,
|
|
307
|
-
timestamp: timestamp >>> 0,
|
|
308
|
-
payloadType: 96,
|
|
309
|
-
ssrc,
|
|
310
|
-
marker: nalType === 1 || nalType === 5, // marker on last NAL of frame
|
|
311
|
-
});
|
|
312
|
-
try {
|
|
313
|
-
videoTrack.writeRtp(new werift.RtpPacket(header, nalData));
|
|
314
|
-
} catch {}
|
|
315
|
-
} else {
|
|
316
|
-
// FU-A fragmentation (RFC 6184)
|
|
317
|
-
const fnri = nalData[0]! & 0xe0; // F + NRI bits
|
|
318
|
-
const nalTypeVal = nalData[0]! & 0x1f;
|
|
319
|
-
const fuIndicator = fnri | 28; // FU-A type = 28
|
|
320
|
-
let pos = 1; // skip NAL header byte
|
|
321
|
-
let isFirst = true;
|
|
322
|
-
|
|
323
|
-
while (pos < nalData.length) {
|
|
324
|
-
const end = Math.min(pos + RTP_MTU - 2, nalData.length); // -2 for FU indicator + FU header
|
|
325
|
-
const isLast = end >= nalData.length;
|
|
326
|
-
|
|
327
|
-
let fuHeader = nalTypeVal;
|
|
328
|
-
if (isFirst) fuHeader |= 0x80; // Start bit
|
|
329
|
-
if (isLast) fuHeader |= 0x40; // End bit
|
|
330
|
-
|
|
331
|
-
const payload = Buffer.concat([
|
|
332
|
-
Buffer.from([fuIndicator, fuHeader]),
|
|
333
|
-
nalData.subarray(pos, end),
|
|
334
|
-
]);
|
|
335
|
-
|
|
336
|
-
const header = new werift.RtpHeader({
|
|
337
|
-
sequenceNumber: seqNum++ & 0xffff,
|
|
338
|
-
timestamp: timestamp >>> 0,
|
|
339
|
-
payloadType: 96,
|
|
340
|
-
ssrc,
|
|
341
|
-
marker: isLast && (nalType === 1 || nalType === 5),
|
|
342
|
-
});
|
|
343
|
-
try {
|
|
344
|
-
videoTrack.writeRtp(new werift.RtpPacket(header, payload));
|
|
345
|
-
} catch {}
|
|
346
|
-
|
|
347
|
-
isFirst = false;
|
|
348
|
-
pos = end;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
const extractNalUnits = (flush = false) => {
|
|
354
|
-
// Find NAL unit boundaries (0x00000001 or 0x000001)
|
|
355
|
-
const units: Buffer[] = [];
|
|
356
|
-
let start = -1;
|
|
357
|
-
|
|
358
|
-
for (let i = 0; i < nalBuffer.length - 3; i++) {
|
|
359
|
-
if (nalBuffer[i] === 0 && nalBuffer[i + 1] === 0) {
|
|
360
|
-
let scLen = 0;
|
|
361
|
-
if (nalBuffer[i + 2] === 0 && i + 3 < nalBuffer.length && nalBuffer[i + 3] === 1) {
|
|
362
|
-
scLen = 4;
|
|
363
|
-
} else if (nalBuffer[i + 2] === 1) {
|
|
364
|
-
scLen = 3;
|
|
365
|
-
}
|
|
366
|
-
if (scLen > 0) {
|
|
367
|
-
if (start >= 0) {
|
|
368
|
-
units.push(nalBuffer.subarray(start, i));
|
|
369
|
-
}
|
|
370
|
-
start = i;
|
|
371
|
-
i += scLen - 1; // skip past start code
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// On flush (ffmpeg exit), emit the trailing NAL unit too
|
|
377
|
-
if (flush && start >= 0) {
|
|
378
|
-
units.push(nalBuffer.subarray(start));
|
|
379
|
-
nalBuffer = Buffer.alloc(0);
|
|
380
|
-
return units;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (start >= 0) {
|
|
384
|
-
// Keep remaining data from last start code onward
|
|
385
|
-
nalBuffer = nalBuffer.subarray(start);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Safety cap: if buffer grows beyond 4MB without producing NALs, discard
|
|
389
|
-
if (nalBuffer.length > 4 * 1024 * 1024 && units.length === 0) {
|
|
390
|
-
process.stderr.write(`[screen-share] NAL buffer exceeded 4MB, resetting\n`);
|
|
391
|
-
nalBuffer = Buffer.alloc(0);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return units;
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
this.ffmpeg.stdout?.on("data", (chunk: Buffer) => {
|
|
398
|
-
if (!this.active) return;
|
|
399
|
-
nalBuffer = Buffer.concat([nalBuffer, chunk]);
|
|
400
|
-
|
|
401
|
-
const units = extractNalUnits();
|
|
402
|
-
for (const unit of units) {
|
|
403
|
-
sendNalUnit(unit);
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
this.ffmpeg.stderr?.on("data", (data: Buffer) => {
|
|
408
|
-
const msg = data.toString();
|
|
409
|
-
if (msg.includes("Error") || msg.includes("error")) {
|
|
410
|
-
process.stderr.write(`[screen-share:ffmpeg] ${msg}\n`);
|
|
411
|
-
}
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
this.ffmpeg.on("exit", (code) => {
|
|
415
|
-
// Flush any remaining NAL unit in the buffer
|
|
416
|
-
const remaining = extractNalUnits(true);
|
|
417
|
-
for (const unit of remaining) {
|
|
418
|
-
sendNalUnit(unit);
|
|
419
|
-
}
|
|
420
|
-
if (this.active) {
|
|
421
|
-
process.stderr.write(`[screen-share] ffmpeg exited with code ${code}\n`);
|
|
422
|
-
this.stop();
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { Envelope } from "@linkshell/protocol";
|
|
2
|
-
|
|
3
|
-
export class ScrollbackBuffer {
|
|
4
|
-
private buffer: Envelope[] = [];
|
|
5
|
-
private readonly capacity: number;
|
|
6
|
-
|
|
7
|
-
constructor(capacity = 1000) {
|
|
8
|
-
this.capacity = capacity;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
push(envelope: Envelope): void {
|
|
12
|
-
if (this.buffer.length >= this.capacity) {
|
|
13
|
-
this.buffer.shift();
|
|
14
|
-
}
|
|
15
|
-
this.buffer.push(envelope);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
replayFrom(seq: number): Envelope[] {
|
|
19
|
-
return this.buffer.filter(
|
|
20
|
-
(e) => e.seq !== undefined && e.seq > seq,
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
trimUpTo(seq: number): void {
|
|
25
|
-
const idx = this.buffer.findIndex(
|
|
26
|
-
(e) => e.seq !== undefined && e.seq > seq,
|
|
27
|
-
);
|
|
28
|
-
if (idx > 0) {
|
|
29
|
-
this.buffer.splice(0, idx);
|
|
30
|
-
} else if (idx === -1) {
|
|
31
|
-
this.buffer = [];
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
get size(): number {
|
|
36
|
-
return this.buffer.length;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
get lastSeq(): number {
|
|
40
|
-
const last = this.buffer[this.buffer.length - 1];
|
|
41
|
-
return last?.seq ?? -1;
|
|
42
|
-
}
|
|
43
|
-
}
|