replicas-computer 0.3.5 → 0.3.6

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/index.js CHANGED
@@ -2937,7 +2937,7 @@ async function computerStatusCommand() {
2937
2937
  }
2938
2938
 
2939
2939
  // src/version.ts
2940
- var COMPUTER_USE_VERSION = "0.3.5";
2940
+ var COMPUTER_USE_VERSION = "0.3.6";
2941
2941
  var COMPUTER_USE_PROTOCOL_VERSION = 1;
2942
2942
 
2943
2943
  // src/program.ts
@@ -3755,6 +3755,62 @@ var InputBridge = class {
3755
3755
  }
3756
3756
  };
3757
3757
 
3758
+ // ../shared/src/desktop-stream.ts
3759
+ var DEFAULT_STREAM_REQUEST = {
3760
+ transport: "webrtc",
3761
+ profile: "balanced",
3762
+ adaptive: true,
3763
+ iceTransportPolicy: "all"
3764
+ };
3765
+ function parseStreamRequest(query, defaults = DEFAULT_STREAM_REQUEST) {
3766
+ const transport = query.get("transport");
3767
+ const profile = query.get("profile");
3768
+ const adapt = query.get("adapt");
3769
+ const ice = query.get("ice");
3770
+ return {
3771
+ transport: transport === "webrtc" || transport === "websocket" ? transport : defaults.transport,
3772
+ profile: profile === "text" || profile === "balanced" ? profile : defaults.profile,
3773
+ adaptive: adapt === "0" ? false : adapt === "1" ? true : defaults.adaptive,
3774
+ iceTransportPolicy: ice === "relay" || ice === "all" ? ice : defaults.iceTransportPolicy
3775
+ };
3776
+ }
3777
+ var H264_PROFILES = {
3778
+ high444: { payloadType: 99, profileLevelId: "f4001f", codec: "avc1.F4002A", label: "High 4:4:4" },
3779
+ high: { payloadType: 96, profileLevelId: "64001f", codec: "avc1.64002A", label: "High" },
3780
+ main: { payloadType: 97, profileLevelId: "4d001f", codec: "avc1.4D002A", label: "Main" },
3781
+ baseline: { payloadType: 98, profileLevelId: "42e01f", codec: "avc1.42E02A", label: "Baseline" }
3782
+ };
3783
+ var PROFILE_ORDER = ["high444", "high", "main", "baseline"];
3784
+ var DESKTOP_WEBRTC_FAILURE_REASONS = {
3785
+ server_unavailable: "server has no WebRTC",
3786
+ browser_unavailable: "browser has no WebRTC",
3787
+ connection_failed: "WebRTC connection failed",
3788
+ server_connection_failed: "Server WebRTC connection failed",
3789
+ codec_unsupported: "browser cannot receive H.264 over WebRTC",
3790
+ negotiation_failed: "negotiation failed",
3791
+ video_timeout: "WebRTC did not decode video in time",
3792
+ video_stalled: "WebRTC video stopped arriving"
3793
+ };
3794
+ function isDesktopWebRtcFailure(value) {
3795
+ if (!isRecord(value) || typeof value.reasonCode !== "string" || !Object.hasOwn(DESKTOP_WEBRTC_FAILURE_REASONS, value.reasonCode) || typeof value.turnConfigured !== "boolean") return false;
3796
+ return [
3797
+ "id",
3798
+ "connectionState",
3799
+ "iceConnectionState",
3800
+ "iceGatheringState",
3801
+ "signalingState",
3802
+ "iceTransportPolicy",
3803
+ "serverVersion",
3804
+ "serverUnavailableReason",
3805
+ "localCandidateTypes",
3806
+ "remoteCandidateTypes",
3807
+ "iceErrorCodes",
3808
+ "errorName",
3809
+ "path",
3810
+ "codec"
3811
+ ].every((key) => typeof value[key] === "string" && value[key].length <= 200) && ["elapsedMs", "candidateErrors", "framesDecoded", "packetsReceived", "packetsLost", "bytesReceived", "lastFrameAgeMs", "encoderFps"].every((key) => typeof value[key] === "number" && Number.isFinite(value[key]));
3812
+ }
3813
+
3758
3814
  // src/input-validation.ts
3759
3815
  var finite = (value) => typeof value === "number" && Number.isFinite(value);
3760
3816
  var optionalFinite = (value) => value === void 0 || finite(value);
@@ -3780,7 +3836,7 @@ function validInput(value) {
3780
3836
  case "candidate":
3781
3837
  return typeof value.candidate === "string" && value.candidate.length < 1024 && typeof value.mid === "string" && value.mid.length < 16;
3782
3838
  case "fallback":
3783
- return typeof value.reason === "string" && value.reason.length < 200;
3839
+ return typeof value.reason === "string" && value.reason.length < 200 && (value.failure === void 0 || isDesktopWebRtcFailure(value.failure));
3784
3840
  case "pointer":
3785
3841
  return finite(value.x) && finite(value.y);
3786
3842
  case "button":
@@ -3801,33 +3857,6 @@ function parseInput(raw) {
3801
3857
  return validInput(value) ? value : void 0;
3802
3858
  }
3803
3859
 
3804
- // ../shared/src/desktop-stream.ts
3805
- var DEFAULT_STREAM_REQUEST = {
3806
- transport: "webrtc",
3807
- profile: "balanced",
3808
- adaptive: true,
3809
- iceTransportPolicy: "all"
3810
- };
3811
- function parseStreamRequest(query, defaults = DEFAULT_STREAM_REQUEST) {
3812
- const transport = query.get("transport");
3813
- const profile = query.get("profile");
3814
- const adapt = query.get("adapt");
3815
- const ice = query.get("ice");
3816
- return {
3817
- transport: transport === "webrtc" || transport === "websocket" ? transport : defaults.transport,
3818
- profile: profile === "text" || profile === "balanced" ? profile : defaults.profile,
3819
- adaptive: adapt === "0" ? false : adapt === "1" ? true : defaults.adaptive,
3820
- iceTransportPolicy: ice === "relay" || ice === "all" ? ice : defaults.iceTransportPolicy
3821
- };
3822
- }
3823
- var H264_PROFILES = {
3824
- high444: { payloadType: 99, profileLevelId: "f4001f", codec: "avc1.F4002A", label: "High 4:4:4" },
3825
- high: { payloadType: 96, profileLevelId: "64001f", codec: "avc1.64002A", label: "High" },
3826
- main: { payloadType: 97, profileLevelId: "4d001f", codec: "avc1.4D002A", label: "Main" },
3827
- baseline: { payloadType: 98, profileLevelId: "42e01f", codec: "avc1.42E02A", label: "Baseline" }
3828
- };
3829
- var PROFILE_ORDER = ["high444", "high", "main", "baseline"];
3830
-
3831
3860
  // src/media-protocol.ts
3832
3861
  var STREAM_PROTOCOL_VERSION = 3;
3833
3862
  var MEDIA_PACKET_HEADER_BYTES = 12;
@@ -4371,6 +4400,7 @@ async function runMediaServer() {
4371
4400
  let videoBytes = 0;
4372
4401
  let audioBytes = 0;
4373
4402
  const events = [];
4403
+ const webrtcFailures = [];
4374
4404
  const samples = [];
4375
4405
  const record = (event, detail) => {
4376
4406
  if (events.push({ at: Date.now(), event, detail }) > eventHistory) events.shift();
@@ -4904,7 +4934,14 @@ async function runMediaServer() {
4904
4934
  record("keyframe", reason);
4905
4935
  void videoEncoder.replace();
4906
4936
  };
4907
- const enterFallback = (viewer, reason) => {
4937
+ const enterFallback = (viewer, reason, failure) => {
4938
+ if (failure && !viewer.failureReported && viewer.request.transport === "webrtc") {
4939
+ viewer.failureReported = true;
4940
+ const entry = { ...failure, at: Date.now() };
4941
+ if (webrtcFailures.push(entry) > 50) webrtcFailures.shift();
4942
+ process.stderr.write(`[desktop-stream] webrtc fallback diagnostics: ${JSON.stringify(entry)}
4943
+ `);
4944
+ }
4908
4945
  if (viewer.fallback) return;
4909
4946
  viewer.fallback = true;
4910
4947
  viewer.waitingForKeyframe = true;
@@ -5001,7 +5038,7 @@ async function runMediaServer() {
5001
5038
  viewer.rtc?.candidate(message.candidate, message.mid);
5002
5039
  return;
5003
5040
  case "fallback":
5004
- enterFallback(viewer, message.reason);
5041
+ enterFallback(viewer, message.reason, message.failure);
5005
5042
  return;
5006
5043
  case "keyframe":
5007
5044
  if (!viewer.fallback) requestKeyframe("viewer watchdog", void 0, viewer);
@@ -5016,6 +5053,8 @@ async function runMediaServer() {
5016
5053
  const config = {
5017
5054
  type: "config",
5018
5055
  protocolVersion: STREAM_PROTOCOL_VERSION,
5056
+ serverVersion: COMPUTER_USE_VERSION,
5057
+ ...!webrtc ? { webrtcUnavailableReason: webrtcEnabled ? "native_module_unavailable" : "disabled" } : {},
5019
5058
  inputRace: true,
5020
5059
  width,
5021
5060
  height,
@@ -5070,7 +5109,7 @@ async function runMediaServer() {
5070
5109
  width,
5071
5110
  height,
5072
5111
  fps,
5073
- ...url.pathname === "/telemetry" ? { events, samples } : {}
5112
+ ...url.pathname === "/telemetry" ? { events, samples, webrtcFailures } : {}
5074
5113
  }));
5075
5114
  return;
5076
5115
  }
@@ -5168,9 +5207,10 @@ async function runMediaServer() {
5168
5207
  });
5169
5208
  } catch (error) {
5170
5209
  record("webrtc error", `could not create a peer connection: ${error instanceof Error ? error.message : String(error)}`);
5210
+ reply({ type: "webrtc-failed" });
5171
5211
  }
5172
5212
  }
5173
- if (!viewer.rtc) enterFallback(viewer, offerWebRtc ? "webrtc unavailable" : "websocket requested");
5213
+ if (!viewer.rtc) enterFallback(viewer, request.transport === "webrtc" ? "webrtc unavailable" : "websocket requested");
5174
5214
  ensureEncoders();
5175
5215
  socket.on("message", (raw, binary) => {
5176
5216
  if (binary) return;
@@ -3,39 +3,6 @@ function isRecord(value) {
3
3
  return typeof value === "object" && value !== null && !Array.isArray(value);
4
4
  }
5
5
 
6
- // src/input-validation.ts
7
- var finite = (value) => typeof value === "number" && Number.isFinite(value);
8
- var iceServer = (value) => {
9
- if (!isRecord(value)) return false;
10
- return typeof value.urls === "string" && (value.username === void 0 || typeof value.username === "string") && (value.credential === void 0 || typeof value.credential === "string");
11
- };
12
- function validServerMessage(value) {
13
- if (!isRecord(value) || typeof value.type !== "string") return false;
14
- switch (value.type) {
15
- case "ping":
16
- case "pong":
17
- return finite(value.sentAt);
18
- case "webrtc-failed":
19
- return true;
20
- case "offer":
21
- return typeof value.sdp === "string" && value.sdp.length < 65536;
22
- case "candidate":
23
- return typeof value.candidate === "string" && value.candidate.length < 1024 && typeof value.mid === "string" && value.mid.length < 16;
24
- case "config":
25
- return finite(value.protocolVersion) && (value.inputRace === void 0 || typeof value.inputRace === "boolean") && finite(value.width) && finite(value.height) && finite(value.fps) && typeof value.videoCodec === "string" && typeof value.audioCodec === "string" && finite(value.audioSampleRate) && finite(value.audioChannels) && isRecord(value.request) && (value.iceServers === void 0 || Array.isArray(value.iceServers) && value.iceServers.every(iceServer));
26
- case "telemetry":
27
- return isRecord(value.encoder) && isRecord(value.cpu) && isRecord(value.viewer) && Array.isArray(value.events);
28
- default:
29
- return false;
30
- }
31
- }
32
-
33
- // src/wheel.ts
34
- function wheelDeltaPixels(event, pageHeight) {
35
- const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 32 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? pageHeight : 1;
36
- return { deltaX: event.deltaX * scale, deltaY: event.deltaY * scale };
37
- }
38
-
39
6
  // ../shared/src/desktop-stream.ts
40
7
  var DEFAULT_STREAM_REQUEST = {
41
8
  transport: "webrtc",
@@ -67,6 +34,49 @@ function h264ProfileLabel(sdpFmtpLine) {
67
34
  return match?.label ?? id ?? "";
68
35
  }
69
36
  var DESKTOP_TRANSPORT_MESSAGE = "replicas-desktop-transport";
37
+ var DESKTOP_WEBRTC_FAILURE_REASONS = {
38
+ server_unavailable: "server has no WebRTC",
39
+ browser_unavailable: "browser has no WebRTC",
40
+ connection_failed: "WebRTC connection failed",
41
+ server_connection_failed: "Server WebRTC connection failed",
42
+ codec_unsupported: "browser cannot receive H.264 over WebRTC",
43
+ negotiation_failed: "negotiation failed",
44
+ video_timeout: "WebRTC did not decode video in time",
45
+ video_stalled: "WebRTC video stopped arriving"
46
+ };
47
+
48
+ // src/input-validation.ts
49
+ var finite = (value) => typeof value === "number" && Number.isFinite(value);
50
+ var iceServer = (value) => {
51
+ if (!isRecord(value)) return false;
52
+ return typeof value.urls === "string" && (value.username === void 0 || typeof value.username === "string") && (value.credential === void 0 || typeof value.credential === "string");
53
+ };
54
+ function validServerMessage(value) {
55
+ if (!isRecord(value) || typeof value.type !== "string") return false;
56
+ switch (value.type) {
57
+ case "ping":
58
+ case "pong":
59
+ return finite(value.sentAt);
60
+ case "webrtc-failed":
61
+ return true;
62
+ case "offer":
63
+ return typeof value.sdp === "string" && value.sdp.length < 65536;
64
+ case "candidate":
65
+ return typeof value.candidate === "string" && value.candidate.length < 1024 && typeof value.mid === "string" && value.mid.length < 16;
66
+ case "config":
67
+ return finite(value.protocolVersion) && (value.serverVersion === void 0 || typeof value.serverVersion === "string") && (value.webrtcUnavailableReason === void 0 || typeof value.webrtcUnavailableReason === "string") && (value.inputRace === void 0 || typeof value.inputRace === "boolean") && finite(value.width) && finite(value.height) && finite(value.fps) && typeof value.videoCodec === "string" && typeof value.audioCodec === "string" && finite(value.audioSampleRate) && finite(value.audioChannels) && isRecord(value.request) && (value.iceServers === void 0 || Array.isArray(value.iceServers) && value.iceServers.every(iceServer));
68
+ case "telemetry":
69
+ return isRecord(value.encoder) && isRecord(value.cpu) && isRecord(value.viewer) && Array.isArray(value.events);
70
+ default:
71
+ return false;
72
+ }
73
+ }
74
+
75
+ // src/wheel.ts
76
+ function wheelDeltaPixels(event, pageHeight) {
77
+ const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 32 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? pageHeight : 1;
78
+ return { deltaX: event.deltaX * scale, deltaY: event.deltaY * scale };
79
+ }
70
80
 
71
81
  // src/media-protocol.ts
72
82
  var STREAM_PROTOCOL_VERSION = 3;
@@ -138,6 +148,13 @@ globalThis.__replicasDesktop = state;
138
148
  var viewerEvents = [];
139
149
  state.events = viewerEvents;
140
150
  var decoderResets = 0;
151
+ var connectionStartedAt = performance.now();
152
+ var failure;
153
+ var localCandidateTypes = /* @__PURE__ */ new Set();
154
+ var remoteCandidateTypes = /* @__PURE__ */ new Set();
155
+ var iceErrorCodes = /* @__PURE__ */ new Set();
156
+ var candidateErrors = 0;
157
+ var negotiationErrorName = "";
141
158
  var note = (event, detail) => {
142
159
  if (viewerEvents.push({ at: Date.now(), event, detail }) > 50) viewerEvents.shift();
143
160
  };
@@ -208,7 +225,7 @@ var sendInput = (message, unreliable = false) => {
208
225
  };
209
226
  var announceTransport = () => {
210
227
  if (window.parent === window) return;
211
- window.parent.postMessage({ type: DESKTOP_TRANSPORT_MESSAGE, transport: transport ?? "connecting", reason: transportReason, path: route.path, codec: route.codec }, "*");
228
+ window.parent.postMessage({ type: DESKTOP_TRANSPORT_MESSAGE, transport: transport ?? "connecting", reason: transportReason, path: route.path, codec: route.codec, failure }, "*");
212
229
  };
213
230
  var closePeer = (preserveDeadline = false) => {
214
231
  if (!preserveDeadline) {
@@ -379,8 +396,38 @@ var unlockAudio = () => {
379
396
  });
380
397
  });
381
398
  };
382
- var useFallback = (reason, failed = false) => {
399
+ var useFallback = (reasonCode, failed = false) => {
383
400
  if (transport === "websocket" || !config) return;
401
+ const reason = reasonCode === "websocket_selected" ? "WebSocket transport selected" : reasonCode === "recent_failure" ? "recent WebRTC failure" : DESKTOP_WEBRTC_FAILURE_REASONS[reasonCode];
402
+ if (request.transport === "webrtc" && reasonCode !== "websocket_selected" && reasonCode !== "recent_failure") {
403
+ failure = {
404
+ id: crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`,
405
+ reasonCode,
406
+ elapsedMs: Math.round(performance.now() - connectionStartedAt),
407
+ connectionState: peer?.connectionState ?? "unavailable",
408
+ iceConnectionState: peer?.iceConnectionState ?? "unavailable",
409
+ iceGatheringState: peer?.iceGatheringState ?? "unavailable",
410
+ signalingState: peer?.signalingState ?? "unavailable",
411
+ iceTransportPolicy: request.iceTransportPolicy,
412
+ turnConfigured: config.iceServers?.some((server) => /^turns?:/i.test(server.urls)) ?? false,
413
+ serverVersion: config.serverVersion ?? "unknown",
414
+ serverUnavailableReason: config.webrtcUnavailableReason ?? "",
415
+ localCandidateTypes: [...localCandidateTypes].join(","),
416
+ remoteCandidateTypes: [...remoteCandidateTypes].join(","),
417
+ iceErrorCodes: [...iceErrorCodes].join(","),
418
+ candidateErrors,
419
+ errorName: negotiationErrorName,
420
+ path: route.path,
421
+ codec: route.codec,
422
+ framesDecoded: inbound?.framesDecoded ?? 0,
423
+ packetsReceived: inbound?.packetsReceived ?? 0,
424
+ packetsLost: inbound?.packetsLost ?? 0,
425
+ bytesReceived: inbound?.bytesReceived ?? 0,
426
+ lastFrameAgeMs: lastFrameAt ? Math.round(performance.now() - lastFrameAt) : -1,
427
+ encoderFps: telemetry?.encoder.fps ?? -1
428
+ };
429
+ state.lastWebRtcFailure = failure;
430
+ }
384
431
  closePeer();
385
432
  if (failed) webrtcSkips = webrtcSkipsAfterFailure;
386
433
  note("fallback to socket", reason);
@@ -388,11 +435,11 @@ var useFallback = (reason, failed = false) => {
388
435
  transportReason = reason;
389
436
  route = { ...route, path: "proxy", codec: config.videoCodec };
390
437
  announceTransport();
438
+ sendSocket({ type: "fallback", reason, failure });
391
439
  if (!TrackGenerator) {
392
440
  showStatus("This browser cannot render the desktop stream");
393
441
  return;
394
442
  }
395
- sendSocket({ type: "fallback", reason });
396
443
  resetVideoTrack();
397
444
  void configureVideo(config).catch((error) => showStatus(describe(error)));
398
445
  void screen.play().catch(() => showStatus("Click the stream to begin playback"));
@@ -427,8 +474,14 @@ var negotiate = async (sdp) => {
427
474
  };
428
475
  pc.onicecandidate = (event) => {
429
476
  if (peer !== pc) return;
477
+ if (event.candidate?.type) localCandidateTypes.add(event.candidate.type);
430
478
  if (event.candidate?.candidate) sendSocket({ type: "candidate", candidate: event.candidate.candidate, mid: event.candidate.sdpMid ?? "0" });
431
479
  };
480
+ pc.onicecandidateerror = (event) => {
481
+ if (peer !== pc) return;
482
+ iceErrorCodes.add(event.errorCode);
483
+ note("ice candidate error", String(event.errorCode));
484
+ };
432
485
  pc.oniceconnectionstatechange = () => {
433
486
  if (peer === pc) note("ice", pc.iceConnectionState);
434
487
  };
@@ -444,7 +497,7 @@ var negotiate = async (sdp) => {
444
497
  unlockAudio();
445
498
  } else if (pc.connectionState === "failed") {
446
499
  if (transport === "webrtc") socket?.close();
447
- else useFallback("WebRTC connection failed", true);
500
+ else useFallback("connection_failed", true);
448
501
  }
449
502
  };
450
503
  try {
@@ -456,7 +509,7 @@ var negotiate = async (sdp) => {
456
509
  if (peer !== pc || !answer.sdp) return;
457
510
  const video = answer.sdp.split(/^m=/m).find((section) => section.startsWith("video")) ?? "";
458
511
  if (video.startsWith("video 0 ") || !/^a=rtpmap:\d+ H264\/90000/m.test(video)) {
459
- useFallback("browser cannot receive H.264 over WebRTC", true);
512
+ useFallback("codec_unsupported", true);
460
513
  return;
461
514
  }
462
515
  sendSocket({ type: "answer", sdp: answer.sdp });
@@ -484,15 +537,22 @@ var handleServerMessage = (message, viaSocket) => {
484
537
  return;
485
538
  }
486
539
  if (message.type === "webrtc-failed") {
487
- useFallback("Server WebRTC connection failed", true);
540
+ useFallback("server_connection_failed", true);
488
541
  return;
489
542
  }
490
543
  if (message.type === "offer") {
491
- void negotiate(message.sdp).catch((error) => useFallback(`negotiation failed: ${describe(error)}`, true));
544
+ void negotiate(message.sdp).catch((error) => {
545
+ negotiationErrorName = error instanceof Error ? error.name : "unknown";
546
+ useFallback("negotiation_failed", true);
547
+ });
492
548
  return;
493
549
  }
494
550
  if (message.type === "candidate") {
495
- void peer?.addIceCandidate({ candidate: message.candidate, sdpMid: message.mid }).catch(() => {
551
+ const candidateType = message.candidate.match(/\btyp (host|srflx|prflx|relay)\b/)?.[1];
552
+ if (candidateType) remoteCandidateTypes.add(candidateType);
553
+ const pc = peer;
554
+ void pc?.addIceCandidate({ candidate: message.candidate, sdpMid: message.mid }).catch(() => {
555
+ if (peer === pc) candidateErrors += 1;
496
556
  });
497
557
  return;
498
558
  }
@@ -504,15 +564,15 @@ var handleServerMessage = (message, viaSocket) => {
504
564
  config = message;
505
565
  request = message.request;
506
566
  fitScreen();
507
- if (request.transport !== "webrtc") useFallback("WebSocket transport selected");
508
- else if (!message.iceServers) useFallback("server has no WebRTC");
509
- else if (typeof RTCPeerConnection !== "function") useFallback("browser has no WebRTC");
567
+ if (request.transport !== "webrtc") useFallback("websocket_selected");
568
+ else if (!message.iceServers) useFallback("server_unavailable");
569
+ else if (typeof RTCPeerConnection !== "function") useFallback("browser_unavailable");
510
570
  else if (webrtcSkips > 0) {
511
571
  webrtcSkips -= 1;
512
- useFallback("recent WebRTC failure");
572
+ useFallback("recent_failure");
513
573
  } else if (!transport) {
514
574
  showStatus("Establishing connection\u2026");
515
- webrtcDeadline ??= setTimeout(() => useFallback("WebRTC did not decode video in time", true), webrtcFrameTimeoutMs);
575
+ webrtcDeadline ??= setTimeout(() => useFallback("video_timeout", true), webrtcFrameTimeoutMs);
516
576
  }
517
577
  };
518
578
  var handleServerText = (data, viaSocket) => {
@@ -529,6 +589,19 @@ var connect = () => {
529
589
  clearTimeout(reconnectTimer);
530
590
  closePeer();
531
591
  transport = void 0;
592
+ transportReason = "";
593
+ failure = void 0;
594
+ config = void 0;
595
+ telemetry = void 0;
596
+ connectionStartedAt = performance.now();
597
+ localCandidateTypes.clear();
598
+ remoteCandidateTypes.clear();
599
+ iceErrorCodes.clear();
600
+ candidateErrors = 0;
601
+ negotiationErrorName = "";
602
+ lastFrameAt = 0;
603
+ route = { path: "", codec: "", decoded: "", kbps: 0, displayedFps: 0 };
604
+ announceTransport();
532
605
  socket?.close();
533
606
  showStatus(reconnectAttempt ? "reconnecting\u2026" : "connecting\u2026");
534
607
  const query = new URLSearchParams([...params].filter(([key]) => ["transport", "profile", "adapt", "ice"].includes(key)));
@@ -754,7 +827,7 @@ setInterval(() => {
754
827
  if (screen.paused && screen.srcObject instanceof MediaStream && screen.srcObject.getVideoTracks().some((track) => track.readyState === "live")) return;
755
828
  const now = performance.now();
756
829
  if (now - lastFrameAt >= webrtcFrameTimeoutMs) {
757
- useFallback("WebRTC video stopped arriving", true);
830
+ useFallback("video_stalled", true);
758
831
  return;
759
832
  }
760
833
  if (inputChannel?.readyState !== "open") return;
@@ -778,7 +851,7 @@ setInterval(() => {
778
851
  displayedWindow = { at: now, frames: presentedFrames };
779
852
  }
780
853
  const elapsedSeconds = (now - renderedWindowStartedAt) / 1e3;
781
- if (transport !== "webrtc" && elapsedSeconds < 1) return;
854
+ if (!peer && transport !== "webrtc" && elapsedSeconds < 1) return;
782
855
  const stats = {
783
856
  sequence: ++statsSequence,
784
857
  videoFps: renderedFrames / elapsedSeconds,
@@ -789,7 +862,7 @@ setInterval(() => {
789
862
  renderedFrames = 0;
790
863
  decodedAudioPackets = 0;
791
864
  renderedWindowStartedAt = now;
792
- if (transport === "webrtc" && peer) void reportWebRtcStats(peer, stats).catch(() => {
865
+ if (peer) void reportWebRtcStats(peer, stats).catch(() => {
793
866
  });
794
867
  else if (transport === "websocket") {
795
868
  sendInput({ type: "stats", ...stats });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-computer",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Replicas workspace computer-use runtime",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",