@skylandnpm/octopus-proxy 0.1.45 → 0.1.47
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/{chunk-SI2GJOTX.js → chunk-ARGKTEJ4.js} +2 -2
- package/dist/{chunk-MVOWNUHT.js → chunk-ZS2F5EU4.js} +16 -4
- package/dist/cli.js +3 -3
- package/dist/main.js +2 -2
- package/dist/remote-worker.js +75 -44
- package/native/remote-macos.m +32 -4
- package/package.json +1 -1
- package/proxy.example.json +2 -2
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
probeRemote,
|
|
15
15
|
verifyHostPassword,
|
|
16
16
|
z
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-ZS2F5EU4.js";
|
|
18
18
|
|
|
19
19
|
// ../../node_modules/.pnpm/ajv@8.17.1/node_modules/ajv/dist/compile/codegen/code.js
|
|
20
20
|
var require_code = __commonJS({
|
|
@@ -13342,7 +13342,7 @@ var RemoteManager = class {
|
|
|
13342
13342
|
}
|
|
13343
13343
|
}, 1e3);
|
|
13344
13344
|
session.monitor.unref();
|
|
13345
|
-
return { remoteId: session.remoteId, bindToken, expiresAt: new Date(expiresAt).toISOString(), display, revision: 1, stunUrls: this.stunUrls(), budget: { ...REMOTE_DEFAULTS, totalBitrate: this.config.totalBitrate, videoMaxBitrate: this.config.videoMaxBitrate }, protocolVersion: 1 };
|
|
13345
|
+
return { remoteId: session.remoteId, bindToken, expiresAt: new Date(expiresAt).toISOString(), display, revision: 1, stunUrls: this.stunUrls(), budget: { ...REMOTE_DEFAULTS, totalBitrate: this.config.totalBitrate, videoMaxBitrate: this.config.videoMaxBitrate }, protocolVersion: 1, heartbeatAck: true };
|
|
13346
13346
|
} catch (error) {
|
|
13347
13347
|
this.finish(session);
|
|
13348
13348
|
throw error;
|
|
@@ -4086,7 +4086,7 @@ var z = /* @__PURE__ */ Object.freeze({
|
|
|
4086
4086
|
});
|
|
4087
4087
|
|
|
4088
4088
|
// src/remote-protocol.ts
|
|
4089
|
-
var REMOTE_DEFAULTS = Object.freeze({ totalBitrate:
|
|
4089
|
+
var REMOTE_DEFAULTS = Object.freeze({ totalBitrate: 3e6, videoMaxBitrate: 24e5, clipboardBitrate: 5e4, maxWidth: 2560, maxHeight: 1440, maxFps: 30 });
|
|
4090
4090
|
var stunUrl = z.string().max(256).regex(/^stun:(?:\[[0-9a-fA-F:]+\]|[a-zA-Z0-9.-]+)(?::[0-9]{1,5})?$/, "\u4EC5\u5141\u8BB8 stun: \u4E3B\u673A[:\u7AEF\u53E3]\uFF0C\u7981\u6B62 TURN").refine((url) => {
|
|
4091
4091
|
const port = /:(\d+)$/.exec(url);
|
|
4092
4092
|
return !port || Number(port[1]) >= 1 && Number(port[1]) <= 65535;
|
|
@@ -4094,8 +4094,8 @@ var stunUrl = z.string().max(256).regex(/^stun:(?:\[[0-9a-fA-F:]+\]|[a-zA-Z0-9.-
|
|
|
4094
4094
|
var RemoteConfigSchema = z.object({
|
|
4095
4095
|
enabled: z.boolean().default(true),
|
|
4096
4096
|
stunUrls: z.array(stunUrl).min(1).max(4).optional(),
|
|
4097
|
-
totalBitrate: z.number().int().min(1e5).max(
|
|
4098
|
-
videoMaxBitrate: z.number().int().min(5e4).max(
|
|
4097
|
+
totalBitrate: z.number().int().min(1e5).max(3e6).default(REMOTE_DEFAULTS.totalBitrate),
|
|
4098
|
+
videoMaxBitrate: z.number().int().min(5e4).max(24e5).default(REMOTE_DEFAULTS.videoMaxBitrate),
|
|
4099
4099
|
ffmpeg: z.string().min(1).default("ffmpeg")
|
|
4100
4100
|
}).strict().refine((c) => c.videoMaxBitrate <= c.totalBitrate * 0.8, "\u89C6\u9891\u989D\u5EA6\u4E0D\u5F97\u8D85\u8FC7\u603B\u9884\u7B97\u7684 80%");
|
|
4101
4101
|
var SignalSchema = z.discriminatedUnion("kind", [
|
|
@@ -4120,7 +4120,7 @@ var InputSchema = z.discriminatedUnion("kind", [
|
|
|
4120
4120
|
z.object({ ...envelope, kind: z.literal("ping") }).strict()
|
|
4121
4121
|
]);
|
|
4122
4122
|
function capVideoSdp(sdp, bitrate) {
|
|
4123
|
-
const limit = Math.max(5e4, Math.min(
|
|
4123
|
+
const limit = Math.max(5e4, Math.min(24e5, Math.floor(bitrate)));
|
|
4124
4124
|
const lines = sdp.split(/\r?\n/), result = [];
|
|
4125
4125
|
let video = false;
|
|
4126
4126
|
for (const line of lines) {
|
|
@@ -4138,6 +4138,17 @@ function capVideoSdp(sdp, bitrate) {
|
|
|
4138
4138
|
return `${result.join("\r\n")}\r
|
|
4139
4139
|
`;
|
|
4140
4140
|
}
|
|
4141
|
+
var RemoteInputOrder = class {
|
|
4142
|
+
sequence = /* @__PURE__ */ new Map();
|
|
4143
|
+
positionFence = -1;
|
|
4144
|
+
accept(channel, input) {
|
|
4145
|
+
if (input.seq <= (this.sequence.get(channel) ?? -1)) return false;
|
|
4146
|
+
this.sequence.set(channel, input.seq);
|
|
4147
|
+
if (input.kind === "pointer" && input.seq <= this.positionFence) return false;
|
|
4148
|
+
if (input.kind === "button" || input.kind === "wheel" || input.kind === "release") this.positionFence = input.seq;
|
|
4149
|
+
return true;
|
|
4150
|
+
}
|
|
4151
|
+
};
|
|
4141
4152
|
function fitVideo(width, height, tier = 0) {
|
|
4142
4153
|
const [w, h] = [[2560, 1440], [1920, 1080], [1280, 720]][tier] ?? [1280, 720];
|
|
4143
4154
|
const scale = Math.min(1, w / width, h / height);
|
|
@@ -4751,6 +4762,7 @@ export {
|
|
|
4751
4762
|
assertDirectSignal,
|
|
4752
4763
|
InputSchema,
|
|
4753
4764
|
capVideoSdp,
|
|
4765
|
+
RemoteInputOrder,
|
|
4754
4766
|
fitVideo,
|
|
4755
4767
|
ensureHostAuth,
|
|
4756
4768
|
verifyHostPassword,
|
package/dist/cli.js
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
supportedPlatform,
|
|
30
30
|
systemdUnitPath,
|
|
31
31
|
toolSearchDirs
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-ARGKTEJ4.js";
|
|
33
33
|
import {
|
|
34
34
|
DESKTOP_PERMISSION_GUIDANCE,
|
|
35
35
|
PlatformDesktop,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
checkDesktop,
|
|
38
38
|
ensureHostAuth,
|
|
39
39
|
requestDesktopPermission
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-ZS2F5EU4.js";
|
|
41
41
|
|
|
42
42
|
// src/cli.ts
|
|
43
43
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -321,7 +321,7 @@ async function runDoctor(opts) {
|
|
|
321
321
|
}, () => false);
|
|
322
322
|
try {
|
|
323
323
|
const status = await remote.status();
|
|
324
|
-
record("P2P \u8FDC\u7A0B\u63A7\u5236", status.available ? "ok" : "warn", status.available ?
|
|
324
|
+
record("P2P \u8FDC\u7A0B\u63A7\u5236", status.available ? "ok" : "warn", status.available ? `\u7EAF STUN\uFF1B\u603B\u9884\u7B97 ${status.budget.totalBitrate / 1e6} Mbps\u3001\u89C6\u9891\u6700\u591A ${status.budget.videoMaxBitrate / 1e3} kbps\uFF1B\u670D\u52A1\u6743\u9650\u4EE5\u5B9E\u9645\u8FDE\u63A5\u9A8C\u8BC1\u4E3A\u51C6` : status.message);
|
|
325
325
|
} finally {
|
|
326
326
|
remote.close();
|
|
327
327
|
}
|
package/dist/main.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ConfigError,
|
|
4
4
|
runDaemon
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-ARGKTEJ4.js";
|
|
6
|
+
import "./chunk-ZS2F5EU4.js";
|
|
7
7
|
|
|
8
8
|
// src/main.ts
|
|
9
9
|
runDaemon(process.env.OCTOPUS_PROXY_CONFIG ?? "proxy.json").catch((error) => {
|
package/dist/remote-worker.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
InputSchema,
|
|
4
4
|
REMOTE_DEFAULTS,
|
|
5
|
+
RemoteInputOrder,
|
|
5
6
|
SignalSchema,
|
|
6
7
|
assertDirectSignal,
|
|
7
8
|
capVideoSdp,
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
nativeProcess,
|
|
10
11
|
parseXrandr,
|
|
11
12
|
terminateNative
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-ZS2F5EU4.js";
|
|
13
14
|
|
|
14
15
|
// src/remote-worker.ts
|
|
15
16
|
import rtc from "@roamhq/wrtc";
|
|
@@ -55,7 +56,7 @@ var RemoteBudget = class {
|
|
|
55
56
|
this.clipboardPaused = true;
|
|
56
57
|
this.quiet = 0;
|
|
57
58
|
this.videoBitrate = Math.max(5e4, Math.floor(Math.min(this.videoBitrate * 0.8, (available ?? this.total) * 0.7)));
|
|
58
|
-
if (this.fpsIndex <
|
|
59
|
+
if (this.fpsIndex < 4) this.fpsIndex++;
|
|
59
60
|
else if (this.tier < 2) this.tier++;
|
|
60
61
|
} else {
|
|
61
62
|
if (this.bitrate < this.total * 0.75 && rtt < 0.2) this.clipboardPaused = false;
|
|
@@ -71,7 +72,7 @@ var RemoteBudget = class {
|
|
|
71
72
|
return before !== `${this.tier}:${this.fpsIndex}:${this.videoBitrate}`;
|
|
72
73
|
}
|
|
73
74
|
video(width, height) {
|
|
74
|
-
return { ...fitVideo(width, height, this.tier), fps: [15, 10, 5][this.fpsIndex], videoBitrate: this.videoBitrate };
|
|
75
|
+
return { ...fitVideo(width, height, this.tier), fps: [30, 20, 15, 10, 5][this.fpsIndex], videoBitrate: this.videoBitrate };
|
|
75
76
|
}
|
|
76
77
|
};
|
|
77
78
|
var TokenBucket = class {
|
|
@@ -90,6 +91,40 @@ var TokenBucket = class {
|
|
|
90
91
|
}
|
|
91
92
|
};
|
|
92
93
|
|
|
94
|
+
// src/remote-liveness.ts
|
|
95
|
+
var RemoteLiveness = class {
|
|
96
|
+
constructor(now = Date.now(), heartbeatTimeout = 5e3) {
|
|
97
|
+
this.heartbeatTimeout = heartbeatTimeout;
|
|
98
|
+
this.lastHeartbeat = now;
|
|
99
|
+
}
|
|
100
|
+
lastHeartbeat;
|
|
101
|
+
disconnectedAt = 0;
|
|
102
|
+
restartDeadline = 0;
|
|
103
|
+
heartbeat(now) {
|
|
104
|
+
this.lastHeartbeat = now;
|
|
105
|
+
}
|
|
106
|
+
disconnect(now) {
|
|
107
|
+
this.disconnectedAt ||= now;
|
|
108
|
+
}
|
|
109
|
+
tick(now, direct) {
|
|
110
|
+
const action = { release: false, restart: false, expired: false, recovered: false };
|
|
111
|
+
if (this.disconnectedAt && direct && this.lastHeartbeat > this.disconnectedAt) {
|
|
112
|
+
this.disconnectedAt = 0;
|
|
113
|
+
this.restartDeadline = 0;
|
|
114
|
+
action.recovered = true;
|
|
115
|
+
}
|
|
116
|
+
if (!this.disconnectedAt && now - this.lastHeartbeat >= this.heartbeatTimeout) this.disconnectedAt = this.lastHeartbeat;
|
|
117
|
+
if (this.disconnectedAt && now - this.disconnectedAt >= 5e3) {
|
|
118
|
+
if (!this.restartDeadline) {
|
|
119
|
+
this.restartDeadline = now + 2e4;
|
|
120
|
+
action.release = true;
|
|
121
|
+
action.restart = true;
|
|
122
|
+
} else if (now >= this.restartDeadline) action.expired = true;
|
|
123
|
+
}
|
|
124
|
+
return action;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
93
128
|
// src/remote-worker.ts
|
|
94
129
|
var exec = promisify(execFile);
|
|
95
130
|
var post = (kind, data = {}) => {
|
|
@@ -115,10 +150,8 @@ var RemotePeer = class {
|
|
|
115
150
|
this.pc.ondatachannel = (e) => this.channel(e.channel);
|
|
116
151
|
this.pc.onconnectionstatechange = () => {
|
|
117
152
|
if (this.pc.connectionState === "failed" || this.pc.connectionState === "disconnected") {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
this.direct = false;
|
|
121
|
-
}
|
|
153
|
+
this.liveness.disconnect(Date.now());
|
|
154
|
+
this.direct = false;
|
|
122
155
|
}
|
|
123
156
|
};
|
|
124
157
|
this.handshake = setTimeout(() => this.stop("\u5F53\u524D\u7F51\u7EDC\u65E0\u6CD5\u5EFA\u7ACB P2P \u8FDE\u63A5\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u4E0E UDP \u9632\u706B\u5899\u540E\u91CD\u8BD5"), 2e4);
|
|
@@ -146,15 +179,12 @@ var RemotePeer = class {
|
|
|
146
179
|
ended = false;
|
|
147
180
|
direct = false;
|
|
148
181
|
binding = false;
|
|
149
|
-
|
|
182
|
+
inputOrder = new RemoteInputOrder();
|
|
150
183
|
candidates = [];
|
|
151
184
|
signalQueue = Promise.resolve();
|
|
152
185
|
timer;
|
|
153
186
|
handshake;
|
|
154
|
-
|
|
155
|
-
disconnectedAt = 0;
|
|
156
|
-
restarted = false;
|
|
157
|
-
restartDeadline = 0;
|
|
187
|
+
liveness = new RemoteLiveness(Date.now(), 15e3);
|
|
158
188
|
sampling = false;
|
|
159
189
|
inputWindow = 0;
|
|
160
190
|
inputCount = 0;
|
|
@@ -189,8 +219,12 @@ var RemotePeer = class {
|
|
|
189
219
|
await this.pc.addIceCandidate(signal.candidate ?? void 0);
|
|
190
220
|
return;
|
|
191
221
|
}
|
|
192
|
-
|
|
193
|
-
this.direct
|
|
222
|
+
const credentials = (sdp) => sdp.split(/\r?\n/).filter((line) => /^a=(?:ice-ufrag|ice-pwd|fingerprint):/.test(line)).join("\n");
|
|
223
|
+
const sameTransport = this.ready && this.direct && this.pc.remoteDescription && credentials(this.pc.remoteDescription.sdp) === credentials(signal.sdp);
|
|
224
|
+
if (!sameTransport) {
|
|
225
|
+
if (this.ready) this.writeNative("release");
|
|
226
|
+
this.direct = false;
|
|
227
|
+
}
|
|
194
228
|
stage = "remote-description";
|
|
195
229
|
await this.pc.setRemoteDescription({ type: "offer", sdp: capVideoSdp(signal.sdp, this.budget.videoBitrate) });
|
|
196
230
|
for (const candidate of this.candidates.splice(0)) await this.pc.addIceCandidate(candidate ?? void 0);
|
|
@@ -223,7 +257,6 @@ var RemotePeer = class {
|
|
|
223
257
|
return;
|
|
224
258
|
}
|
|
225
259
|
if (!this.ready || value.remoteId !== this.init.remoteId || value.revision !== 1) throw new Error("ownership");
|
|
226
|
-
if (!this.direct) return;
|
|
227
260
|
const now = Date.now();
|
|
228
261
|
if (now - this.inputWindow > 1e3) {
|
|
229
262
|
this.inputWindow = now;
|
|
@@ -233,18 +266,17 @@ var RemotePeer = class {
|
|
|
233
266
|
if (channel.label !== "clipboard") this.inputBytes += Buffer.byteLength(event.data);
|
|
234
267
|
if (++this.inputCount > 240 || this.inputBytes > 2e4) throw new Error("rate");
|
|
235
268
|
if (channel.label === "clipboard") {
|
|
236
|
-
this.clipboard(value);
|
|
269
|
+
if (this.direct && !this.liveness.disconnectedAt) this.clipboard(value);
|
|
237
270
|
return;
|
|
238
271
|
}
|
|
239
272
|
const input = InputSchema.parse(value);
|
|
240
273
|
if (channel.label === "pointer" !== (input.kind === "pointer")) throw new Error("channel");
|
|
241
|
-
|
|
242
|
-
if (input.seq <= seq) return;
|
|
243
|
-
this.sequence.set(channel.label, input.seq);
|
|
274
|
+
if (!this.inputOrder.accept(channel.label, input)) return;
|
|
244
275
|
if (input.kind === "ping") {
|
|
245
|
-
this.
|
|
276
|
+
void this.heartbeat(now).catch(() => this.stop("\u8FDC\u63A7\u5FC3\u8DF3\u6821\u9A8C\u5931\u8D25"));
|
|
246
277
|
return;
|
|
247
278
|
}
|
|
279
|
+
if (input.kind !== "release" && (!this.direct || this.liveness.disconnectedAt)) return;
|
|
248
280
|
this.input(input);
|
|
249
281
|
if (input.kind !== "release") post("activity");
|
|
250
282
|
} catch {
|
|
@@ -252,6 +284,13 @@ var RemotePeer = class {
|
|
|
252
284
|
}
|
|
253
285
|
};
|
|
254
286
|
}
|
|
287
|
+
async heartbeat(now) {
|
|
288
|
+
if (this.pc.connectionState !== "connected" || !await this.verifyDirect() || this.ended) return;
|
|
289
|
+
this.direct = true;
|
|
290
|
+
this.liveness.heartbeat(now);
|
|
291
|
+
if (this.liveness.tick(now, true).recovered) post("state", { state: "connected" });
|
|
292
|
+
this.send("control", { kind: "pong" });
|
|
293
|
+
}
|
|
255
294
|
async verifyDirect() {
|
|
256
295
|
const stats = await this.pc.getStats();
|
|
257
296
|
const transport = [...stats.values()].find((s) => s.type === "transport" && s.selectedCandidatePairId);
|
|
@@ -284,7 +323,7 @@ var RemotePeer = class {
|
|
|
284
323
|
if (this.ended) return;
|
|
285
324
|
this.captureStartedAt = Date.now();
|
|
286
325
|
this.ready = true;
|
|
287
|
-
this.
|
|
326
|
+
this.liveness = new RemoteLiveness(Date.now(), value.heartbeatAck === true ? 5e3 : 15e3);
|
|
288
327
|
clearTimeout(this.handshake);
|
|
289
328
|
this.send("control", { kind: "ready", revision: 1, video: this.video });
|
|
290
329
|
post("state", { state: "connected" });
|
|
@@ -533,31 +572,23 @@ var RemotePeer = class {
|
|
|
533
572
|
this.stop("\u4E3B\u673A\u672A\u8F93\u51FA\u684C\u9762\u753B\u9762\uFF0C\u8BF7\u68C0\u67E5\u5F55\u5C4F\u6743\u9650\u548C\u56FE\u5F62\u4F1A\u8BDD");
|
|
534
573
|
return;
|
|
535
574
|
}
|
|
536
|
-
if (this.ready
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if (
|
|
575
|
+
if (this.ready) this.writeNative("ping");
|
|
576
|
+
this.direct = this.pc.connectionState === "connected" && await this.verifyDirect();
|
|
577
|
+
if (this.ended || !this.ready) return;
|
|
578
|
+
const action = this.liveness.tick(now, this.direct);
|
|
579
|
+
if (action.release) {
|
|
541
580
|
this.writeNative("release");
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
post("state", { state: "restart" });
|
|
546
|
-
} else if (now > this.restartDeadline) {
|
|
547
|
-
this.stop("\u5F53\u524D\u7F51\u7EDC\u65E0\u6CD5\u6062\u590D P2P \u8FDE\u63A5");
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
581
|
+
if (this.pendingClipboard) this.send("clipboard", { kind: "clipboard.error", id: this.pendingClipboard.id, message: "\u7F51\u7EDC\u4E2D\u65AD\uFF0C\u526A\u8D34\u677F\u4F20\u8F93\u5DF2\u53D6\u6D88\uFF0C\u8BF7\u91CD\u8BD5" });
|
|
582
|
+
this.pendingClipboard = void 0;
|
|
583
|
+
this.nativePending.clear();
|
|
550
584
|
}
|
|
551
|
-
if (
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
this.disconnectedAt = 0;
|
|
556
|
-
this.lastPing = now;
|
|
557
|
-
post("state", { state: "connected" });
|
|
585
|
+
if (action.restart) post("state", { state: "restart" });
|
|
586
|
+
if (action.expired) {
|
|
587
|
+
this.stop("\u5F53\u524D\u7F51\u7EDC\u65E0\u6CD5\u6062\u590D P2P \u8FDE\u63A5");
|
|
588
|
+
return;
|
|
558
589
|
}
|
|
559
|
-
if (
|
|
560
|
-
this.
|
|
590
|
+
if (action.recovered) post("state", { state: "connected" });
|
|
591
|
+
if (!this.direct || this.liveness.disconnectedAt) return;
|
|
561
592
|
const stats = await this.pc.getStats(), counters = transportBytes(stats.values());
|
|
562
593
|
if (this.budget.sample(now, counters.bytes, counters.available, counters.rtt)) {
|
|
563
594
|
const video = this.budget.video(this.init.display.width, this.init.display.height);
|
|
@@ -576,7 +607,7 @@ var RemotePeer = class {
|
|
|
576
607
|
if (fps === void 0 && this.encodedFrames && now > this.encodedFrames.at) fps = Math.max(0, (outbound.framesEncoded - this.encodedFrames.count) * 1e3 / (now - this.encodedFrames.at));
|
|
577
608
|
this.encodedFrames = { at: now, count: outbound.framesEncoded };
|
|
578
609
|
}
|
|
579
|
-
const actualVideo = { ...this.video, width: outbound?.frameWidth ?? this.video.width, height: outbound?.frameHeight ?? this.video.height, fps: Math.min(
|
|
610
|
+
const actualVideo = { ...this.video, width: outbound?.frameWidth ?? this.video.width, height: outbound?.frameHeight ?? this.video.height, fps: Math.min(30, Math.round(fps ?? 0)) };
|
|
580
611
|
this.send("control", { kind: "stats", video: actualVideo, bitrate: Math.round(this.budget.bitrate), clipboardPaused: this.budget.clipboardPaused });
|
|
581
612
|
} finally {
|
|
582
613
|
this.sampling = false;
|
package/native/remote-macos.m
CHANGED
|
@@ -41,9 +41,10 @@ static CGEventFlags flags(void) {
|
|
|
41
41
|
return value;
|
|
42
42
|
}
|
|
43
43
|
static void keyEvent(CGKeyCode code, BOOL down) {
|
|
44
|
+
BOOL repeat=down && [heldKeys containsObject:@(code)];
|
|
44
45
|
if (down) [heldKeys addObject:@(code)]; else [heldKeys removeObject:@(code)];
|
|
45
46
|
CGEventRef e = CGEventCreateKeyboardEvent(NULL, code, down);
|
|
46
|
-
if (e) { CGEventSetFlags(e,flags()); CGEventPost(kCGHIDEventTap,e); CFRelease(e); }
|
|
47
|
+
if (e) { CGEventSetFlags(e,flags()); CGEventSetIntegerValueField(e,kCGKeyboardEventAutorepeat,repeat); CGEventPost(kCGHIDEventTap,e); CFRelease(e); }
|
|
47
48
|
}
|
|
48
49
|
static CGEventType mouseType(int button, BOOL down) {
|
|
49
50
|
if (button == 0) return down ? kCGEventLeftMouseDown : kCGEventLeftMouseUp;
|
|
@@ -77,8 +78,15 @@ static BOOL writeAll(int fd,const void *data,size_t length) {
|
|
|
77
78
|
@interface RemoteStream : NSObject <SCStreamOutput,SCStreamDelegate>
|
|
78
79
|
@property(nonatomic,strong) SCStream *stream;
|
|
79
80
|
@property(nonatomic,strong) SCStreamConfiguration *configuration;
|
|
81
|
+
@property(nonatomic,strong) NSData *pendingFrame;
|
|
82
|
+
@property(nonatomic,strong) dispatch_queue_t writerQueue;
|
|
83
|
+
@property(nonatomic) BOOL writing;
|
|
80
84
|
@end
|
|
81
85
|
@implementation RemoteStream
|
|
86
|
+
- (instancetype)init {
|
|
87
|
+
if ((self=[super init])) self.writerQueue=dispatch_queue_create("octopus.remote.writer",DISPATCH_QUEUE_SERIAL);
|
|
88
|
+
return self;
|
|
89
|
+
}
|
|
82
90
|
- (void)stream:(SCStream *)stream didStopWithError:(NSError *)error { stopping=1; }
|
|
83
91
|
- (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
|
|
84
92
|
if (stopping || type!=SCStreamOutputTypeScreen || !CMSampleBufferIsValid(sampleBuffer)) return;
|
|
@@ -97,7 +105,27 @@ static BOOL writeAll(int fd,const void *data,size_t length) {
|
|
|
97
105
|
for(size_t row=0;row<h/2;row++) for(size_t col=0;col<w/2;col++) { u[row*w/2+col]=uv[row*uvs+col*2]; v[row*w/2+col]=uv[row*uvs+col*2+1]; }
|
|
98
106
|
CVPixelBufferUnlockBaseAddress(pixel,kCVPixelBufferLock_ReadOnly);
|
|
99
107
|
uint32_t header[3]={htonl((uint32_t)w),htonl((uint32_t)h),htonl((uint32_t)length)};
|
|
100
|
-
|
|
108
|
+
NSMutableData *frame=[NSMutableData dataWithBytes:header length:sizeof(header)]; [frame appendData:data];
|
|
109
|
+
// Pipe backpressure must not block ScreenCaptureKit's capture queue. Bound
|
|
110
|
+
// memory to one writing frame plus one replaceable latest frame.
|
|
111
|
+
@synchronized(self) {
|
|
112
|
+
self.pendingFrame=frame;
|
|
113
|
+
if(self.writing)return;
|
|
114
|
+
self.writing=YES;
|
|
115
|
+
}
|
|
116
|
+
dispatch_async(self.writerQueue,^{
|
|
117
|
+
@autoreleasepool {
|
|
118
|
+
while(!stopping) {
|
|
119
|
+
NSData *latest;
|
|
120
|
+
@synchronized(self) {
|
|
121
|
+
latest=self.pendingFrame; self.pendingFrame=nil;
|
|
122
|
+
if(!latest) { self.writing=NO; return; }
|
|
123
|
+
}
|
|
124
|
+
if(!writeAll(3,latest.bytes,latest.length))break;
|
|
125
|
+
}
|
|
126
|
+
@synchronized(self) { self.pendingFrame=nil; self.writing=NO; }
|
|
127
|
+
}
|
|
128
|
+
});
|
|
101
129
|
}
|
|
102
130
|
@end
|
|
103
131
|
|
|
@@ -110,7 +138,7 @@ static void command(NSString *line,RemoteStream *capture) {
|
|
|
110
138
|
if ([op isEqual:@"ping"]) { lastPing=NSDate.date.timeIntervalSince1970; return; }
|
|
111
139
|
if ([op isEqual:@"video"] && parts.count==4) {
|
|
112
140
|
NSInteger w=parts[1].integerValue,h=parts[2].integerValue,fps=parts[3].integerValue;
|
|
113
|
-
if(w<2 || h<2 || w>2560 || h>1440 || w%2 || h%2 || fps<1 || fps>
|
|
141
|
+
if(w<2 || h<2 || w>2560 || h>1440 || w%2 || h%2 || fps<1 || fps>30) { stopping=1; return; }
|
|
114
142
|
capture.configuration.width=w; capture.configuration.height=h; capture.configuration.minimumFrameInterval=CMTimeMake(1,(int32_t)fps);
|
|
115
143
|
[capture.stream updateConfiguration:capture.configuration completionHandler:^(NSError *error) { if(error) stopping=1; }]; return;
|
|
116
144
|
}
|
|
@@ -149,7 +177,7 @@ int main(int argc,const char *argv[]) {
|
|
|
149
177
|
if(argc!=6 || strcmp(argv[1],"serve")!=0 || !inputOK() || !CGPreflightScreenCaptureAccess()) return 1;
|
|
150
178
|
displayID=(CGDirectDisplayID)strtoul(argv[2],NULL,10); bounds=CGDisplayBounds(displayID);
|
|
151
179
|
int w=atoi(argv[3]),h=atoi(argv[4]),fps=atoi(argv[5]);
|
|
152
|
-
if(!CGDisplayIsActive(displayID) || w<2 || h<2 || w>2560 || h>1440 || w%2 || h%2 || fps<1 || fps>
|
|
180
|
+
if(!CGDisplayIsActive(displayID) || w<2 || h<2 || w>2560 || h>1440 || w%2 || h%2 || fps<1 || fps>30) return 1;
|
|
153
181
|
heldKeys=[NSMutableSet set]; cursor=bounds.origin; parentPID=getppid(); lastPing=NSDate.date.timeIntervalSince1970;
|
|
154
182
|
signal(SIGTERM,signalStop); signal(SIGINT,signalStop); signal(SIGPIPE,signalStop);
|
|
155
183
|
fcntl(STDIN_FILENO,F_SETFL,O_NONBLOCK);
|
package/package.json
CHANGED