@skylandnpm/octopus-proxy 0.1.45 → 0.1.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.
@@ -14,7 +14,7 @@ import {
14
14
  probeRemote,
15
15
  verifyHostPassword,
16
16
  z
17
- } from "./chunk-MVOWNUHT.js";
17
+ } from "./chunk-GD6G4K6E.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: 1e6, videoMaxBitrate: 8e5, clipboardBitrate: 5e4, maxWidth: 2560, maxHeight: 1440, maxFps: 15 });
4089
+ var REMOTE_DEFAULTS = Object.freeze({ totalBitrate: 2e6, videoMaxBitrate: 16e5, 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(1e6).default(REMOTE_DEFAULTS.totalBitrate),
4098
- videoMaxBitrate: z.number().int().min(5e4).max(8e5).default(REMOTE_DEFAULTS.videoMaxBitrate),
4097
+ totalBitrate: z.number().int().min(1e5).max(2e6).default(REMOTE_DEFAULTS.totalBitrate),
4098
+ videoMaxBitrate: z.number().int().min(5e4).max(16e5).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(8e5, Math.floor(bitrate)));
4123
+ const limit = Math.max(5e4, Math.min(16e5, 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-SI2GJOTX.js";
32
+ } from "./chunk-3QQWM23K.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-MVOWNUHT.js";
40
+ } from "./chunk-GD6G4K6E.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 ? "\u7EAF STUN\uFF1B\u603B\u9884\u7B97 1 Mbps\u3001\u89C6\u9891\u6700\u591A 800 kbps\uFF1B\u670D\u52A1\u6743\u9650\u4EE5\u5B9E\u9645\u8FDE\u63A5\u9A8C\u8BC1\u4E3A\u51C6" : status.message);
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-SI2GJOTX.js";
6
- import "./chunk-MVOWNUHT.js";
5
+ } from "./chunk-3QQWM23K.js";
6
+ import "./chunk-GD6G4K6E.js";
7
7
 
8
8
  // src/main.ts
9
9
  runDaemon(process.env.OCTOPUS_PROXY_CONFIG ?? "proxy.json").catch((error) => {
@@ -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-MVOWNUHT.js";
13
+ } from "./chunk-GD6G4K6E.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 < 2) 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
- if (!this.disconnectedAt) {
119
- this.disconnectedAt = Date.now();
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
- sequence = /* @__PURE__ */ new Map();
182
+ inputOrder = new RemoteInputOrder();
150
183
  candidates = [];
151
184
  signalQueue = Promise.resolve();
152
185
  timer;
153
186
  handshake;
154
- lastPing = Date.now();
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
- if (this.ready) this.writeNative("release");
193
- this.direct = false;
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
- const seq = this.sequence.get(channel.label) ?? -1;
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.lastPing = now;
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.lastPing = Date.now();
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 && now - this.lastPing > 15e3 && !this.disconnectedAt) {
537
- this.stop("\u8FDC\u63A7\u5BA2\u6237\u7AEF\u5DF2\u79BB\u7EBF");
538
- return;
539
- }
540
- if (this.disconnectedAt && now - this.disconnectedAt >= 5e3) {
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 (!this.restarted) {
543
- this.restarted = true;
544
- this.restartDeadline = now + 2e4;
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 (this.pc.connectionState !== "connected") return;
552
- this.direct = await this.verifyDirect();
553
- if (!this.direct) return;
554
- if (this.disconnectedAt) {
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 (!this.ready) return;
560
- this.writeNative("ping");
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(15, Math.round(fps ?? 0)) };
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;
@@ -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
- writeAll(3,header,sizeof(header)); writeAll(3,data.bytes,length);
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>15) { stopping=1; return; }
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>15) return 1;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylandnpm/octopus-proxy",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  "desktopRemote": {
9
9
  "enabled": true,
10
10
  "stunUrls": ["stun:hub.example.com:3478"],
11
- "totalBitrate": 1000000,
12
- "videoMaxBitrate": 800000
11
+ "totalBitrate": 2000000,
12
+ "videoMaxBitrate": 1600000
13
13
  }
14
14
  }