termdock 1.4.335 → 1.4.336

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.
@@ -4,7 +4,7 @@ import net from 'node:net';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  // The overlay implements a private scrcpy ABI: never load it into an unverified server.
6
6
  const SUPPORTED_SERVER_SHA256 = '8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e';
7
- const OVERLAY_SHA256 = '19eee1787a564bd2996a6443613aaa4700185838d4e42fe59d1d08ca6b12e828';
7
+ const OVERLAY_SHA256 = 'f8eb6275ed255eba00c53146e874d22fe1e5766a466ba03d42a3b07df4f99d63';
8
8
  export const bitrateOverlayPath = fileURLToPath(new URL('../../../scripts/android-bitrate/bitrate.jar', import.meta.url));
9
9
  export const bitrateOverlayRemote = '/data/local/tmp/termdock-bitrate-' + OVERLAY_SHA256.slice(0, 16) + '.jar';
10
10
  export const bundledPreviewServerPath = fileURLToPath(new URL('../../../scripts/android-bitrate/scrcpy-server-v3.3.4', import.meta.url));
@@ -57,6 +57,8 @@ export class LiveBitrateChannel {
57
57
  onReady;
58
58
  server = net.createServer(socket => this.accept(socket));
59
59
  socket = null;
60
+ forwardPort = null;
61
+ retryTimer = null;
60
62
  buffer = Buffer.alloc(0);
61
63
  ready = false;
62
64
  diagnosticProtocol = false;
@@ -72,9 +74,26 @@ export class LiveBitrateChannel {
72
74
  this.server.once('error', reject);
73
75
  this.server.listen(0, '127.0.0.1', resolve);
74
76
  });
75
- this.handshakeTimer = setTimeout(() => this.close('BITRATE_HANDSHAKE_TIMEOUT: no extension handshake within 15s'), 15000);
77
+ this.startHandshakeTimeout();
76
78
  return this.server.address().port;
77
79
  }
80
+ connectForward(port) {
81
+ if (this.closed)
82
+ return;
83
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
84
+ throw new Error('BITRATE_FORWARD_INVALID_PORT');
85
+ this.forwardPort = port;
86
+ this.startHandshakeTimeout();
87
+ this.dialForward();
88
+ }
89
+ startHandshakeTimeout() {
90
+ this.handshakeTimer = setTimeout(() => this.close('BITRATE_HANDSHAKE_TIMEOUT: no extension handshake within 15s'), 15000);
91
+ }
92
+ dialForward() {
93
+ this.retryTimer = null;
94
+ if (!this.closed && this.forwardPort !== null)
95
+ this.accept(net.connect({ host: '127.0.0.1', port: this.forwardPort }));
96
+ }
78
97
  accept(socket) {
79
98
  if (this.socket || this.closed) {
80
99
  socket.destroy();
@@ -82,9 +101,24 @@ export class LiveBitrateChannel {
82
101
  }
83
102
  this.socket = socket;
84
103
  socket.setNoDelay(true);
85
- socket.on('error', error => this.close(`BITRATE_CHANNEL_ERROR: ${error.message}`));
86
- socket.on('close', () => this.close('BITRATE_CHANNEL_CLOSED: device closed the bitrate channel'));
104
+ const disconnected = (detail) => {
105
+ if (this.socket !== socket || this.closed)
106
+ return;
107
+ if (this.forwardPort !== null && !this.ready) {
108
+ // ADB may accept TCP before the device-side abstract socket is listening.
109
+ this.socket = null;
110
+ this.buffer = Buffer.alloc(0);
111
+ socket.destroy();
112
+ this.retryTimer = setTimeout(() => this.dialForward(), 100);
113
+ }
114
+ else
115
+ this.close(detail);
116
+ };
117
+ socket.on('error', error => disconnected(`BITRATE_CHANNEL_ERROR: ${error.message}`));
118
+ socket.on('close', () => disconnected('BITRATE_CHANNEL_CLOSED: device closed the bitrate channel'));
87
119
  socket.on('data', chunk => {
120
+ if (this.socket !== socket || this.closed)
121
+ return;
88
122
  this.buffer = Buffer.concat([this.buffer, chunk]);
89
123
  while (this.buffer.length >= 4 && !this.closed) {
90
124
  const value = this.buffer.readUInt32BE();
@@ -148,6 +182,8 @@ export class LiveBitrateChannel {
148
182
  this.lastFailure = detail;
149
183
  if (this.handshakeTimer)
150
184
  clearTimeout(this.handshakeTimer);
185
+ if (this.retryTimer)
186
+ clearTimeout(this.retryTimer);
151
187
  if (this.pending) {
152
188
  clearTimeout(this.pending.timer);
153
189
  this.pending.resolve(false);
@@ -135,6 +135,7 @@ export class ScrcpySession {
135
135
  // Network ADB can acknowledge reverse rules without carrying their data.
136
136
  tunnelForward;
137
137
  forwardPort = 0;
138
+ bitrateForwardPort = 0;
138
139
  connectionAbort = new AbortController();
139
140
  scid = randomInt(1, 0x7fffffff);
140
141
  bitrateChannel = null;
@@ -217,11 +218,19 @@ export class ScrcpySession {
217
218
  if (!bitrateFailure) {
218
219
  try {
219
220
  this.bitrateChannel = new LiveBitrateChannel(reportBitrate);
220
- const port = await this.bitrateChannel.listen();
221
+ const port = this.tunnelForward ? 0 : await this.bitrateChannel.listen();
221
222
  bitrateStage = 'BITRATE_PUSH_FAILED';
222
223
  await runAdbCapture(['-s', this.serial, 'push', bitrateOverlayPath, bitrateOverlayRemote]);
223
224
  bitrateStage = 'BITRATE_TUNNEL_FAILED';
224
- await runAdbCapture(['-s', this.serial, 'reverse', `localabstract:termdock_bitrate_${scidHex(this.scid)}`, `tcp:${port}`]);
225
+ if (this.tunnelForward) {
226
+ const assigned = Number(await runAdbCapture(['-s', this.serial, 'forward', 'tcp:0', `localabstract:termdock_bitrate_${scidHex(this.scid)}`]));
227
+ if (!Number.isInteger(assigned) || assigned < 1 || assigned > 65535)
228
+ throw new Error('BITRATE_FORWARD_INVALID_PORT');
229
+ this.bitrateForwardPort = assigned;
230
+ }
231
+ else {
232
+ await runAdbCapture(['-s', this.serial, 'reverse', `localabstract:termdock_bitrate_${scidHex(this.scid)}`, `tcp:${port}`]);
233
+ }
225
234
  liveBitrate = true;
226
235
  }
227
236
  catch (error) {
@@ -242,7 +251,7 @@ export class ScrcpySession {
242
251
  const args = [
243
252
  '-s', this.serial, 'shell',
244
253
  `CLASSPATH=${liveBitrate ? bitrateOverlayRemote + ":" : ""}${remoteJar}`,
245
- 'app_process', ...(liveBitrate ? [`-Dtermdock.bitrate=termdock_bitrate_${scidHex(this.scid)}`] : []), '/',
254
+ 'app_process', ...(liveBitrate ? [`-Dtermdock.bitrate=termdock_bitrate_${scidHex(this.scid)}`, ...(this.tunnelForward ? ['-Dtermdock.bitrateForward=true'] : [])] : []), '/',
246
255
  'com.genymobile.scrcpy.Server', version,
247
256
  `scid=${this.scid.toString(16)}`,
248
257
  'log_level=info',
@@ -262,6 +271,8 @@ export class ScrcpySession {
262
271
  void headerReady.catch(() => { });
263
272
  const child = spawn(adbBinary(), args, { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PATH: adbSearchPath() } });
264
273
  this.child = child;
274
+ if (liveBitrate && this.tunnelForward)
275
+ this.bitrateChannel?.connectForward(this.bitrateForwardPort);
265
276
  child.stdout?.on('data', chunk => this.log(String(chunk).replace(/^\[server\]\s*/, '')));
266
277
  child.stderr?.on('data', chunk => this.log(String(chunk)));
267
278
  child.on('error', error => this.fail(error instanceof Error ? error : new Error('SCRCPY_SPAWN_FAILED')));
@@ -444,9 +455,10 @@ export class ScrcpySession {
444
455
  return this.bitrateChannel?.setBitrate(value) ?? Promise.resolve(false);
445
456
  }
446
457
  async removeForward() {
447
- const port = this.forwardPort;
458
+ const ports = [this.forwardPort, this.bitrateForwardPort].filter(port => port > 0);
448
459
  this.forwardPort = 0;
449
- if (port) {
460
+ this.bitrateForwardPort = 0;
461
+ for (const port of ports) {
450
462
  try {
451
463
  await runAdbCapture(['-s', this.serial, 'forward', '--remove', `tcp:${port}`], 8000);
452
464
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termdock",
3
- "version": "1.4.335",
3
+ "version": "1.4.336",
4
4
  "description": "A complete web-based terminal application with modern UI",
5
5
  "type": "module",
6
6
  "main": ".desktop-dist/main.js",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "packageName": "termdock",
4
- "version": "1.4.335",
4
+ "version": "1.4.336",
5
5
  "runtimeProtocolVersion": 1,
6
6
  "minimumDesktopVersion": "1.4.46",
7
7
  "nodeMajor": 22,
8
8
  "dependencyHash": "sha256-oaGYi/P5ob8sd6VnNBOwshKozjhdV+hYiCmLAsn5x00=",
9
- "serverBundleHash": "sha256-oP7hnH+0kNAIOiaQqS+7gVZR8blwiKXULzIZ8eQYCOE=",
9
+ "serverBundleHash": "sha256-KvJEGINN1M45Zs/oNpJTb+vlpFCr0C8rKqS2Flp7+tI=",
10
10
  "clientBundleHash": "sha256-5AO8soGCDDxFlZjr8zTeNxVLFL4bw+MXYLTu2kdyLs0=",
11
11
  "entrypoint": "dist/server/cli.js",
12
12
  "clientEntrypoint": "dist/client/index.html"
@@ -5,6 +5,7 @@ package com.genymobile.scrcpy.video;
5
5
 
6
6
  import android.media.MediaCodec;
7
7
  import android.net.LocalSocket;
8
+ import android.net.LocalServerSocket;
8
9
  import android.net.LocalSocketAddress;
9
10
  import android.os.Bundle;
10
11
  import java.io.DataInputStream;
@@ -59,28 +60,41 @@ public class CaptureReset implements SurfaceCapture.CaptureListener {
59
60
  }
60
61
 
61
62
  private void serve(String name) {
62
- try (LocalSocket socket = new LocalSocket()) {
63
- socket.connect(new LocalSocketAddress(name));
64
- DataInputStream input = new DataInputStream(socket.getInputStream());
65
- DataOutputStream output = new DataOutputStream(socket.getOutputStream());
66
- output.writeInt(0x54444232); // TDB2: rejection includes length-prefixed UTF-8 diagnostic
67
- output.flush();
68
- while (true) {
69
- int requested = input.readInt();
70
- String failure = applyBitrate(requested);
71
- output.writeInt(failure == null ? requested : 0);
72
- if (failure != null) {
73
- byte[] detail = failure.substring(0, Math.min(512, failure.length())).getBytes(StandardCharsets.UTF_8);
74
- output.writeInt(detail.length);
75
- output.write(detail);
63
+ try {
64
+ if (Boolean.getBoolean("termdock.bitrateForward")) {
65
+ try (LocalServerSocket server = new LocalServerSocket(name);
66
+ LocalSocket socket = server.accept()) {
67
+ exchange(socket);
68
+ }
69
+ } else {
70
+ try (LocalSocket socket = new LocalSocket()) {
71
+ socket.connect(new LocalSocketAddress(name));
72
+ exchange(socket);
76
73
  }
77
- output.flush();
78
74
  }
79
75
  } catch (IOException ignored) {
80
76
  // A bitrate-channel failure must never tear down video/control.
81
77
  }
82
78
  }
83
79
 
80
+ private void exchange(LocalSocket socket) throws IOException {
81
+ DataInputStream input = new DataInputStream(socket.getInputStream());
82
+ DataOutputStream output = new DataOutputStream(socket.getOutputStream());
83
+ output.writeInt(0x54444232); // TDB2: rejection includes length-prefixed UTF-8 diagnostic
84
+ output.flush();
85
+ while (true) {
86
+ int requested = input.readInt();
87
+ String failure = applyBitrate(requested);
88
+ output.writeInt(failure == null ? requested : 0);
89
+ if (failure != null) {
90
+ byte[] detail = failure.substring(0, Math.min(512, failure.length())).getBytes(StandardCharsets.UTF_8);
91
+ output.writeInt(detail.length);
92
+ output.write(detail);
93
+ }
94
+ output.flush();
95
+ }
96
+ }
97
+
84
98
  @Override
85
99
  public void onInvalidated() { reset(); }
86
100
  }
@@ -1,10 +1,10 @@
1
1
  # scrcpy live bitrate extension
2
2
 
3
- Termdock loads this 3 KB Android DEX overlay before the bundled, verified scrcpy 3.3.4 server
3
+ Termdock loads this small Android DEX overlay before the bundled, verified scrcpy 3.3.4 server
4
4
  for preview sessions only. The host scrcpy version no longer gates automatic quality. Recording uses the unmodified scrcpy executable/server.
5
5
 
6
6
  CaptureReset.java preserves scrcpy 3.3.4's existing ABI and reset behavior,
7
- adding a session-scoped ADB reverse socket. The running hardware encoder receives
7
+ adding a session-scoped ADB socket. The running hardware encoder receives
8
8
  MediaCodec.PARAMETER_KEY_VIDEO_BITRATE via setParameters, under the same lock
9
9
  used to clear the codec before stop/reset. There is no transcoding, frame polling,
10
10
  forced keyframe, video reset, resolution change or new video connection.
@@ -30,9 +30,11 @@ quality with a diagnostic. Rejections and channel failures also preserve video.
30
30
  Network ADB preview sessions use `adb forward` for video and control; USB
31
31
  sessions retain `adb reverse`. The forward connector waits for the scrcpy dummy
32
32
  byte before opening control and removes only its own allocated TCP rule on stop.
33
- The optional bitrate channel still uses a separate reverse rule. If reverse rules
34
- register but carry no data, its handshake times out with a diagnostic; preview
35
- continues at the configured bitrate.
33
+ The optional bitrate channel uses its own forward rule for network devices; its
34
+ device-side listener accepts the host connection after the encoder starts. USB
35
+ retains its separate reverse rule. Early forward TCP accepts are retried until
36
+ the TDB handshake arrives, within the existing 15-second limit. Bitrate failures
37
+ remain isolated from video/control, and stop removes both session-owned rules.
36
38
 
37
39
  Extend the allowlist only after verifying the CaptureReset ABI and real-device
38
40
  behavior. No global scrcpy installation is modified.
@@ -52,13 +54,17 @@ configuration packet.
52
54
 
53
55
  Mac server and iPhone preview rendering were also verified with network
54
56
  PTP-AN00 / Android 16 and Pixel 9 / Android 15 using forward video. On those cloud
55
- routes, reverse registration succeeded without carrying data: bitrate handshakes
56
- timed out while preview stayed alive. Physical identity properties matched across
57
+ routes, reverse registration succeeded without carrying data; this motivated
58
+ using forward for both video and the separate bitrate channel. Physical identity properties matched across
57
59
  the PTP IPv4/IPv6 routes; option grouping and selected-route preservation are
58
- covered by tests. These observations do not establish live bitrate support on
59
- those devices or full native input/recording acceptance.
60
+ covered by tests. Full native input/recording acceptance remains outside these observations.
61
+ The Pixel 9 forward bitrate channel was verified on the Mac server: all four
62
+ 1.8 → 6 → 0.9 → 4 Mbps requests received matching ACKs in 35–37 ms, with one
63
+ video header and the same scrcpy process throughout a 20-second session. Both
64
+ forward rules were removed on stop. API acceptance does not establish exact
65
+ vendor output bandwidth.
60
66
 
61
- Diagnostics distinguish version/build, missing/corrupt extension, ADB push/reverse,
67
+ Diagnostics distinguish version/build, missing/corrupt extension, ADB push/tunnel,
62
68
  15s handshake timeout, 2.5s device ACK timeout, 4s client confirmation timeout,
63
69
  channel/protocol failure and device codec rejection. Detailed reasons travel over
64
70
  the existing encrypted stream and appear in an expandable warning. Intentional
Binary file