tirtc-devtools-cli 0.0.1

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.
Files changed (156) hide show
  1. package/README.md +52 -0
  2. package/USAGE.md +417 -0
  3. package/bin/tirtc-devtool.js +2 -0
  4. package/dist/app-server/protocol-client/index.d.ts +25 -0
  5. package/dist/app-server/protocol-client/index.js +114 -0
  6. package/dist/devtools/cli/src/config.d.ts +46 -0
  7. package/dist/devtools/cli/src/config.js +98 -0
  8. package/dist/devtools/cli/src/dummy.d.ts +0 -0
  9. package/dist/devtools/cli/src/dummy.js +1 -0
  10. package/dist/devtools/cli/src/embedded_paths.d.ts +7 -0
  11. package/dist/devtools/cli/src/embedded_paths.js +85 -0
  12. package/dist/devtools/cli/src/facade.d.ts +723 -0
  13. package/dist/devtools/cli/src/facade.js +194 -0
  14. package/dist/devtools/cli/src/ffmpeg_tool.d.ts +6 -0
  15. package/dist/devtools/cli/src/ffmpeg_tool.js +146 -0
  16. package/dist/devtools/cli/src/guide.d.ts +1 -0
  17. package/dist/devtools/cli/src/guide.js +49 -0
  18. package/dist/devtools/cli/src/index.d.ts +1 -0
  19. package/dist/devtools/cli/src/index.js +753 -0
  20. package/dist/devtools/cli/src/media_assets.d.ts +25 -0
  21. package/dist/devtools/cli/src/media_assets.js +121 -0
  22. package/dist/devtools/cli/src/session_manager.d.ts +25 -0
  23. package/dist/devtools/cli/src/session_manager.js +393 -0
  24. package/dist/devtools/cli/src/token_tool.d.ts +33 -0
  25. package/dist/devtools/cli/src/token_tool.js +217 -0
  26. package/dist/devtools/cli/src/transport.d.ts +30 -0
  27. package/dist/devtools/cli/src/transport.js +84 -0
  28. package/dist/dummy.d.ts +0 -0
  29. package/dist/dummy.js +1 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +48 -0
  32. package/package.json +54 -0
  33. package/script/ensure_ffmpeg.sh +22 -0
  34. package/vendor/app-server/bin/native/macos-arm64/credential_napi.node +0 -0
  35. package/vendor/app-server/bin/native/macos-arm64/libcrypto.dylib +0 -0
  36. package/vendor/app-server/bin/native/macos-arm64/libssl.dylib +0 -0
  37. package/vendor/app-server/bin/native/macos-arm64/libtgrtc.dylib +0 -0
  38. package/vendor/app-server/bin/native/macos-arm64/runtime_host_napi.node +0 -0
  39. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/audio_codec.h +23 -0
  40. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/audio_frame.h +36 -0
  41. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/audio_io.h +56 -0
  42. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/audio_io_android.h +19 -0
  43. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/audio_io_apple.h +19 -0
  44. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/audio_io_harmony.h +19 -0
  45. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/audio_io_windows.h +19 -0
  46. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/audio_processing.h +56 -0
  47. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/audio_sample_rate.h +18 -0
  48. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/error.h +20 -0
  49. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/logging.h +53 -0
  50. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/media_codec.h +21 -0
  51. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/media_downlink.h +89 -0
  52. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/media_uplink.h +115 -0
  53. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/runtime.h +236 -0
  54. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/video_codec.h +57 -0
  55. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/video_frame.h +55 -0
  56. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/video_io.h +46 -0
  57. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/video_io_android.h +32 -0
  58. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/video_io_apple.h +34 -0
  59. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/video_io_harmony.h +32 -0
  60. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/video_io_windows.h +26 -0
  61. package/vendor/app-server/bin/runtime/linux-x64/include/tirtc/video_processing.h +34 -0
  62. package/vendor/app-server/bin/runtime/linux-x64/lib/libmatrix_runtime_audio.a +0 -0
  63. package/vendor/app-server/bin/runtime/linux-x64/lib/libmatrix_runtime_facade.a +0 -0
  64. package/vendor/app-server/bin/runtime/linux-x64/lib/libmatrix_runtime_foundation_logging.a +0 -0
  65. package/vendor/app-server/bin/runtime/linux-x64/lib/libmatrix_runtime_media.a +0 -0
  66. package/vendor/app-server/bin/runtime/linux-x64/lib/libmatrix_runtime_video.a +0 -0
  67. package/vendor/app-server/bin/runtime/linux-x64/lib/libwebrtc_apm.a +0 -0
  68. package/vendor/app-server/bin/runtime/linux-x64/lib/libxlog.a +0 -0
  69. package/vendor/app-server/bin/runtime/linux-x64/manifest.txt +34 -0
  70. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/audio.h +398 -0
  71. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/audio_codec.h +23 -0
  72. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/audio_frame.h +36 -0
  73. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/audio_io.h +56 -0
  74. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/audio_io_android.h +19 -0
  75. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/audio_io_apple.h +19 -0
  76. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/audio_io_harmony.h +19 -0
  77. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/audio_io_windows.h +19 -0
  78. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/audio_processing.h +56 -0
  79. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/audio_sample_rate.h +18 -0
  80. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/av.h +452 -0
  81. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/credential.h +34 -0
  82. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/error.h +30 -0
  83. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/foundation/build_info.h +27 -0
  84. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/http.h +57 -0
  85. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/logging.h +55 -0
  86. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/media_codec.h +21 -0
  87. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/media_downlink.h +95 -0
  88. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/media_fixture_av_sync.h +61 -0
  89. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/media_fixture_source.h +77 -0
  90. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/media_live_source.h +71 -0
  91. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/media_uplink.h +116 -0
  92. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/transport.h +481 -0
  93. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/trp.h +541 -0
  94. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/video_codec.h +58 -0
  95. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/video_frame.h +55 -0
  96. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/video_io.h +46 -0
  97. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/video_io_android.h +32 -0
  98. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/video_io_apple.h +47 -0
  99. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/video_io_harmony.h +32 -0
  100. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/video_io_windows.h +26 -0
  101. package/vendor/app-server/bin/runtime/macos-arm64/include/tirtc/video_processing.h +34 -0
  102. package/vendor/app-server/bin/runtime/macos-arm64/lib/libcrypto.a +0 -0
  103. package/vendor/app-server/bin/runtime/macos-arm64/lib/libcrypto.dylib +0 -0
  104. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_audio.a +0 -0
  105. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_credential.a +0 -0
  106. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_facade.a +0 -0
  107. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_foundation_http.a +0 -0
  108. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_foundation_logging.a +0 -0
  109. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_media.a +0 -0
  110. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_transport.a +0 -0
  111. package/vendor/app-server/bin/runtime/macos-arm64/lib/libmatrix_runtime_video.a +0 -0
  112. package/vendor/app-server/bin/runtime/macos-arm64/lib/libssl.a +0 -0
  113. package/vendor/app-server/bin/runtime/macos-arm64/lib/libssl.dylib +0 -0
  114. package/vendor/app-server/bin/runtime/macos-arm64/lib/libwebrtc_apm.a +0 -0
  115. package/vendor/app-server/bin/runtime/macos-arm64/lib/libxlog.a +0 -0
  116. package/vendor/app-server/bin/runtime/macos-arm64/manifest.txt +50 -0
  117. package/vendor/app-server/bin/tirtc-devtool-host.js +2 -0
  118. package/vendor/app-server/dist/host/ArtifactManager.d.ts +18 -0
  119. package/vendor/app-server/dist/host/ArtifactManager.js +83 -0
  120. package/vendor/app-server/dist/host/HostProtocol.d.ts +107 -0
  121. package/vendor/app-server/dist/host/HostProtocol.js +256 -0
  122. package/vendor/app-server/dist/host/HostServer.d.ts +49 -0
  123. package/vendor/app-server/dist/host/HostServer.js +635 -0
  124. package/vendor/app-server/dist/host/HostState.d.ts +60 -0
  125. package/vendor/app-server/dist/host/HostState.js +19 -0
  126. package/vendor/app-server/dist/host/RuntimeAdapter.d.ts +81 -0
  127. package/vendor/app-server/dist/host/RuntimeAdapter.js +559 -0
  128. package/vendor/app-server/dist/host/RuntimeCredentialTokenIssuer.d.ts +30 -0
  129. package/vendor/app-server/dist/host/RuntimeCredentialTokenIssuer.js +224 -0
  130. package/vendor/app-server/dist/host/RuntimeReceiveWorker.d.ts +37 -0
  131. package/vendor/app-server/dist/host/RuntimeReceiveWorker.js +186 -0
  132. package/vendor/app-server/dist/host/RuntimeSendWorker.d.ts +42 -0
  133. package/vendor/app-server/dist/host/RuntimeSendWorker.js +274 -0
  134. package/vendor/app-server/dist/host/TokenTool.d.ts +15 -0
  135. package/vendor/app-server/dist/host/TokenTool.js +84 -0
  136. package/vendor/app-server/dist/host/WebPreviewGateway.d.ts +28 -0
  137. package/vendor/app-server/dist/host/WebPreviewGateway.js +815 -0
  138. package/vendor/app-server/dist/host/native/RuntimeCredentialTokenIssuer.d.ts +26 -0
  139. package/vendor/app-server/dist/host/native/RuntimeCredentialTokenIssuer.js +118 -0
  140. package/vendor/app-server/dist/host/native/RuntimeHostBridge.d.ts +19 -0
  141. package/vendor/app-server/dist/host/native/RuntimeHostBridge.js +141 -0
  142. package/vendor/app-server/dist/host/runtime_backed_preflight.d.ts +10 -0
  143. package/vendor/app-server/dist/host/runtime_backed_preflight.js +78 -0
  144. package/vendor/app-server/dist/host/tests/helpers/host_socket_client.d.ts +28 -0
  145. package/vendor/app-server/dist/host/tests/helpers/host_socket_client.js +85 -0
  146. package/vendor/app-server/dist/host/tests/helpers/runtime_e2e_local_config.d.ts +10 -0
  147. package/vendor/app-server/dist/host/tests/helpers/runtime_e2e_local_config.js +41 -0
  148. package/vendor/app-server/dist/host/tests/helpers/runtime_test_env.d.ts +11 -0
  149. package/vendor/app-server/dist/host/tests/helpers/runtime_test_env.js +32 -0
  150. package/vendor/app-server/dist/protocol/contract.d.ts +983 -0
  151. package/vendor/app-server/dist/protocol/contract.js +198 -0
  152. package/vendor/app-server/dist/protocol-client/index.d.ts +25 -0
  153. package/vendor/app-server/dist/protocol-client/index.js +114 -0
  154. package/vendor/app-server/dist/src/index.d.ts +1 -0
  155. package/vendor/app-server/dist/src/index.js +294 -0
  156. package/vendor/runtime/script/prepare_runtime_media_dataset.sh +427 -0
@@ -0,0 +1,815 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.WebPreviewGateway = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const http = __importStar(require("http"));
39
+ const crypto = __importStar(require("crypto"));
40
+ const MAGIC = 0x54525046; // 'TRPF'
41
+ const HEADER_SIZE = 24;
42
+ const POLL_MS = 40;
43
+ function nowIsoString() {
44
+ return new Date().toISOString();
45
+ }
46
+ function parseHeader(buffer) {
47
+ if (buffer.length < HEADER_SIZE) {
48
+ return undefined;
49
+ }
50
+ const magic = buffer.readUInt32LE(0);
51
+ if (magic !== MAGIC) {
52
+ return undefined;
53
+ }
54
+ const version = buffer.readUInt8(4);
55
+ if (version !== 1) {
56
+ return undefined;
57
+ }
58
+ return {
59
+ kind: buffer.readUInt8(5),
60
+ flags: buffer.readUInt8(6),
61
+ streamId: buffer.readUInt32LE(8),
62
+ timestampMs: buffer.readUInt32LE(12),
63
+ payloadLength: buffer.readUInt32LE(16),
64
+ };
65
+ }
66
+ function normalizePreviewMedia(kind, fallback) {
67
+ if (kind >= 65 && kind <= 127) {
68
+ return 'video';
69
+ }
70
+ if (kind > 0 && kind < 65) {
71
+ return 'audio';
72
+ }
73
+ return fallback;
74
+ }
75
+ function renderViewerHtml(endpoint, token) {
76
+ const escapedEndpoint = endpoint.replace(/&/g, '&amp;').replace(/</g, '&lt;');
77
+ return `<!doctype html>
78
+ <html>
79
+ <head>
80
+ <meta charset="utf-8" />
81
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
82
+ <title>TiRTC Preview</title>
83
+ <style>
84
+ body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; background: #0b0f14; color: #d7e0ea; }
85
+ .wrap { display: grid; grid-template-rows: auto 1fr; min-height: 100vh; }
86
+ .top { padding: 10px 14px; border-bottom: 1px solid #1f2a36; background: #111923; }
87
+ .meta { font-size: 12px; color: #9fb0c3; }
88
+ .controls { display: flex; gap: 8px; margin-top: 8px; }
89
+ button { border: 1px solid #2b3a4a; background: #172332; color: #d7e0ea; border-radius: 6px; padding: 6px 10px; cursor: pointer; }
90
+ button:hover { background: #1b2b3d; }
91
+ .stage { display: grid; place-items: center; padding: 12px; }
92
+ canvas { width: min(96vw, 1280px); aspect-ratio: 16 / 9; background: #000; border: 1px solid #1f2a36; }
93
+ </style>
94
+ </head>
95
+ <body>
96
+ <div class="wrap">
97
+ <div class="top">
98
+ <div>TiRTC Local Preview</div>
99
+ <div class="meta">endpoint: ${escapedEndpoint}</div>
100
+ <div class="meta" id="status">status: connecting...</div>
101
+ <div class="controls">
102
+ <button id="start">Start Stream</button>
103
+ <button id="stop">Stop Stream</button>
104
+ </div>
105
+ </div>
106
+ <div class="stage"><canvas id="screen" width="1280" height="720"></canvas></div>
107
+ </div>
108
+ <script>
109
+ (() => {
110
+ const token = ${JSON.stringify(token)};
111
+ const statusEl = document.getElementById('status');
112
+ const canvas = document.getElementById('screen');
113
+ const ctx = canvas.getContext('2d');
114
+ const startBtn = document.getElementById('start');
115
+ const stopBtn = document.getElementById('stop');
116
+
117
+ const AUDIO_SAMPLE_RATE = 16000;
118
+ const AUDIO_TARGET_BUFFER_MS = 120;
119
+ const AUDIO_MAX_BUFFER_MS = 320;
120
+ const VIDEO_TARGET_BUFFER_MS = 100;
121
+ const VIDEO_RENDER_INTERVAL_MS = 33;
122
+ const VIDEO_MAX_QUEUE = 8;
123
+
124
+ const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
125
+ let audioNode = null;
126
+ let audioReady = false;
127
+ let audioQueuedSamples = 0;
128
+ let audioUnderflows = 0;
129
+
130
+ let videoFrames = [];
131
+ let videoBaseTs = null;
132
+ let videoBaseWallMs = 0;
133
+ let videoTimer = null;
134
+
135
+ function setStatus(text) {
136
+ statusEl.textContent = text;
137
+ }
138
+
139
+ function resetVideoTimeline() {
140
+ for (const frame of videoFrames) {
141
+ try { frame.image.close(); } catch (_) {}
142
+ }
143
+ videoFrames = [];
144
+ videoBaseTs = null;
145
+ videoBaseWallMs = 0;
146
+ }
147
+
148
+ function resetAudioTimeline() {
149
+ audioQueuedSamples = 0;
150
+ if (audioNode) {
151
+ audioNode.port.postMessage({type: 'clear'});
152
+ }
153
+ }
154
+
155
+ async function ensureAudioWorklet() {
156
+ if (audioReady && audioNode) {
157
+ return;
158
+ }
159
+ const startThresholdSamples = Math.floor((AUDIO_TARGET_BUFFER_MS * AUDIO_SAMPLE_RATE) / 1000);
160
+ const codeLines = [
161
+ 'class PcmRingProcessor extends AudioWorkletProcessor {',
162
+ ' constructor() {',
163
+ ' super();',
164
+ ' this.queue = [];',
165
+ ' this.offset = 0;',
166
+ ' this.queuedSamples = 0;',
167
+ ' this.underflowCount = 0;',
168
+ ' this.renderCount = 0;',
169
+ ' this.started = false;',
170
+ ' this.startThresholdSamples = 0;',
171
+ ' this.port.onmessage = (event) => {',
172
+ ' const data = event.data || {};',
173
+ ' if (data.type === "configure") {',
174
+ ' this.startThresholdSamples = Number(data.startThresholdSamples || 0);',
175
+ ' return;',
176
+ ' }',
177
+ ' if (data.type === "clear") {',
178
+ ' this.queue = [];',
179
+ ' this.offset = 0;',
180
+ ' this.queuedSamples = 0;',
181
+ ' this.started = false;',
182
+ ' return;',
183
+ ' }',
184
+ ' if (data.type === "pcm" && data.samples) {',
185
+ ' const chunk = new Float32Array(data.samples);',
186
+ ' if (chunk.length > 0) {',
187
+ ' this.queue.push(chunk);',
188
+ ' this.queuedSamples += chunk.length;',
189
+ ' }',
190
+ ' }',
191
+ ' };',
192
+ ' }',
193
+ ' process(inputs, outputs) {',
194
+ ' const output = outputs[0] && outputs[0][0];',
195
+ ' if (!output) { return true; }',
196
+ ' let outIndex = 0;',
197
+ ' if (!this.started && this.queuedSamples >= this.startThresholdSamples) {',
198
+ ' this.started = true;',
199
+ ' }',
200
+ ' if (!this.started) {',
201
+ ' output.fill(0);',
202
+ ' this.renderCount += 1;',
203
+ ' return true;',
204
+ ' }',
205
+ ' while (outIndex < output.length) {',
206
+ ' if (this.queue.length === 0) {',
207
+ ' for (; outIndex < output.length; outIndex += 1) { output[outIndex] = 0; }',
208
+ ' this.underflowCount += 1;',
209
+ ' break;',
210
+ ' }',
211
+ ' const head = this.queue[0];',
212
+ ' const remain = head.length - this.offset;',
213
+ ' const need = output.length - outIndex;',
214
+ ' const copy = remain < need ? remain : need;',
215
+ ' output.set(head.subarray(this.offset, this.offset + copy), outIndex);',
216
+ ' outIndex += copy;',
217
+ ' this.offset += copy;',
218
+ ' this.queuedSamples -= copy;',
219
+ ' if (this.offset >= head.length) {',
220
+ ' this.queue.shift();',
221
+ ' this.offset = 0;',
222
+ ' }',
223
+ ' }',
224
+ ' this.renderCount += 1;',
225
+ ' if ((this.renderCount % 20) === 0) {',
226
+ ' this.port.postMessage({type: "stats", queuedSamples: this.queuedSamples, underflowCount: this.underflowCount, started: this.started});',
227
+ ' }',
228
+ ' return true;',
229
+ ' }',
230
+ '}',
231
+ 'registerProcessor("pcm-ring-processor", PcmRingProcessor);',
232
+ ];
233
+
234
+ const moduleCode = codeLines.join('\\n');
235
+ const blobUrl = URL.createObjectURL(new Blob([moduleCode], {type: 'text/javascript'}));
236
+ await audioCtx.audioWorklet.addModule(blobUrl);
237
+ URL.revokeObjectURL(blobUrl);
238
+
239
+ audioNode = new AudioWorkletNode(audioCtx, 'pcm-ring-processor', {
240
+ numberOfInputs: 0,
241
+ numberOfOutputs: 1,
242
+ outputChannelCount: [1],
243
+ });
244
+ audioNode.port.onmessage = (event) => {
245
+ const data = event.data || {};
246
+ if (data.type === 'stats') {
247
+ audioQueuedSamples = Number(data.queuedSamples || 0);
248
+ audioUnderflows = Number(data.underflowCount || 0);
249
+ }
250
+ };
251
+ audioNode.connect(audioCtx.destination);
252
+ audioNode.port.postMessage({type: 'configure', startThresholdSamples});
253
+ audioReady = true;
254
+ }
255
+
256
+ function enqueueAudioPcm(bytes) {
257
+ if (!audioReady || !audioNode || audioCtx.state !== 'running') {
258
+ return;
259
+ }
260
+ const frameCount = Math.floor(bytes.byteLength / 2);
261
+ if (frameCount <= 0) {
262
+ return;
263
+ }
264
+
265
+ const queuedMs = (audioQueuedSamples * 1000) / AUDIO_SAMPLE_RATE;
266
+ if (queuedMs > AUDIO_MAX_BUFFER_MS) {
267
+ return;
268
+ }
269
+
270
+ const pcm = new Int16Array(frameCount);
271
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
272
+ for (let i = 0; i < frameCount; i += 1) {
273
+ pcm[i] = view.getInt16(i * 2, true);
274
+ }
275
+
276
+ const f32 = new Float32Array(frameCount);
277
+ for (let i = 0; i < frameCount; i += 1) {
278
+ f32[i] = pcm[i] / 32768;
279
+ }
280
+
281
+ audioNode.port.postMessage({type: 'pcm', samples: f32.buffer}, [f32.buffer]);
282
+ audioQueuedSamples += frameCount;
283
+ }
284
+
285
+ function scheduleVideoLoop() {
286
+ if (videoTimer) {
287
+ return;
288
+ }
289
+ videoTimer = setInterval(() => {
290
+ if (videoFrames.length === 0) {
291
+ return;
292
+ }
293
+
294
+ // Always render the freshest decoded frame to avoid timeline stalls
295
+ // when source timestamps are discontinuous.
296
+ const latest = videoFrames.pop();
297
+ while (videoFrames.length > 0) {
298
+ const stale = videoFrames.shift();
299
+ try { stale.image.close(); } catch (_) {}
300
+ }
301
+
302
+ if (!latest) {
303
+ return;
304
+ }
305
+
306
+ if (canvas.width !== latest.image.width || canvas.height !== latest.image.height) {
307
+ canvas.width = latest.image.width;
308
+ canvas.height = latest.image.height;
309
+ }
310
+ ctx.drawImage(latest.image, 0, 0, canvas.width, canvas.height);
311
+ latest.image.close();
312
+ }, VIDEO_RENDER_INTERVAL_MS);
313
+ }
314
+
315
+ async function enqueueVideoJpeg(bytes, timestampMs) {
316
+ const blob = new Blob([bytes], {type: 'image/jpeg'});
317
+ const image = await createImageBitmap(blob);
318
+ videoFrames.push({timestampMs: Number(timestampMs || 0), image});
319
+ videoFrames.sort((a, b) => a.timestampMs - b.timestampMs);
320
+ while (videoFrames.length > VIDEO_MAX_QUEUE) {
321
+ const old = videoFrames.shift();
322
+ try { old.image.close(); } catch (_) {}
323
+ }
324
+ }
325
+
326
+ async function triggerStart() {
327
+ try {
328
+ resetAudioTimeline();
329
+ resetVideoTimeline();
330
+ const response = await fetch('/preview/start?token=' + encodeURIComponent(token));
331
+ if (!response.ok) {
332
+ setStatus('status: start failed: http ' + response.status);
333
+ return;
334
+ }
335
+ const data = await response.json();
336
+ if (!data.ok) {
337
+ setStatus('status: start failed');
338
+ return;
339
+ }
340
+
341
+ // Audio resume/worklet can require user gesture; do not block stream start on it.
342
+ try {
343
+ if (audioCtx.state !== 'running') {
344
+ await audioCtx.resume();
345
+ }
346
+ await ensureAudioWorklet();
347
+ } catch (_) {}
348
+
349
+ setStatus('status: stream requested');
350
+ } catch (e) {
351
+ setStatus('status: start request error: ' + String(e));
352
+ }
353
+ }
354
+
355
+ async function triggerStop() {
356
+ try {
357
+ resetAudioTimeline();
358
+ resetVideoTimeline();
359
+ const response = await fetch('/preview/stop?token=' + encodeURIComponent(token), {keepalive: true});
360
+ if (!response.ok) {
361
+ setStatus('status: stop failed: http ' + response.status);
362
+ return;
363
+ }
364
+ const data = await response.json();
365
+ if (!data.ok) {
366
+ setStatus('status: stop failed');
367
+ return;
368
+ }
369
+ setStatus('status: stopped');
370
+ } catch (e) {
371
+ setStatus('status: stop request error: ' + String(e));
372
+ }
373
+ }
374
+
375
+ scheduleVideoLoop();
376
+ // Auto-start preview when page opens so URL click immediately begins rendering.
377
+ void triggerStart();
378
+
379
+ const ev = new EventSource('/preview/events?token=' + encodeURIComponent(token));
380
+ ev.onopen = () => {
381
+ setStatus('status: connected');
382
+ };
383
+ ev.onerror = () => {
384
+ setStatus('status: reconnecting...');
385
+ };
386
+ ev.addEventListener('frame', (event) => {
387
+ void (async () => {
388
+ try {
389
+ const payload = JSON.parse(event.data);
390
+ const bytes = Uint8Array.from(atob(payload.payloadBase64), (c) => c.charCodeAt(0));
391
+ if (payload.media === 'video') {
392
+ await enqueueVideoJpeg(bytes, Number(payload.timestampMs || 0));
393
+ return;
394
+ }
395
+ if (payload.media === 'audio') {
396
+ enqueueAudioPcm(bytes);
397
+ }
398
+ } catch (e) {
399
+ setStatus('status: frame error: ' + String(e));
400
+ }
401
+ })();
402
+ });
403
+
404
+ if (startBtn) {
405
+ startBtn.addEventListener('click', () => {
406
+ void triggerStart();
407
+ });
408
+ }
409
+ if (stopBtn) {
410
+ stopBtn.addEventListener('click', () => {
411
+ void triggerStop();
412
+ });
413
+ }
414
+
415
+ window.addEventListener('pagehide', () => {
416
+ try { ev.close(); } catch (_) {}
417
+ void triggerStop();
418
+ });
419
+ })();
420
+ </script>
421
+ </body>
422
+ </html>`;
423
+ }
424
+ class WebPreviewGateway {
425
+ server;
426
+ serverStarting;
427
+ pollTimer;
428
+ sources = new Map();
429
+ clients = new Set();
430
+ token = '';
431
+ port = 0;
432
+ parseStreamFilter(raw) {
433
+ if (!raw || raw.trim().length === 0) {
434
+ return undefined;
435
+ }
436
+ const items = raw
437
+ .split(',')
438
+ .map((v) => v.trim())
439
+ .filter((v) => v.length > 0);
440
+ if (items.length === 0) {
441
+ return undefined;
442
+ }
443
+ return new Set(items);
444
+ }
445
+ sourceMatchesFilter(source, filter) {
446
+ if (!filter) {
447
+ return true;
448
+ }
449
+ return filter.has(String(source.streamId)) || filter.has(`${source.streamId}:${source.media}`);
450
+ }
451
+ async startSources(filter) {
452
+ const requestedAt = nowIsoString();
453
+ let matchedSources = 0;
454
+ let stoppedSources = 0;
455
+ let alreadyStartedSources = 0;
456
+ const failedSources = [];
457
+ for (const source of this.sources.values()) {
458
+ if (!this.sourceMatchesFilter(source, filter)) {
459
+ continue;
460
+ }
461
+ matchedSources += 1;
462
+ if (source.started) {
463
+ alreadyStartedSources += 1;
464
+ continue;
465
+ }
466
+ try {
467
+ if (source.onStart) {
468
+ await source.onStart();
469
+ }
470
+ source.started = true;
471
+ stoppedSources += 1;
472
+ }
473
+ catch (error) {
474
+ failedSources.push({
475
+ outputId: source.outputId,
476
+ reason: error instanceof Error ? error.message : String(error),
477
+ });
478
+ }
479
+ }
480
+ return {
481
+ ok: failedSources.length === 0,
482
+ requestedAt,
483
+ matchedSources,
484
+ startedSources: stoppedSources,
485
+ alreadyStartedSources,
486
+ failedSources,
487
+ };
488
+ }
489
+ async stopSources(filter) {
490
+ const requestedAt = nowIsoString();
491
+ let matchedSources = 0;
492
+ let startedSources = 0;
493
+ let alreadyStartedSources = 0;
494
+ const failedSources = [];
495
+ for (const source of this.sources.values()) {
496
+ if (!this.sourceMatchesFilter(source, filter)) {
497
+ continue;
498
+ }
499
+ matchedSources += 1;
500
+ if (!source.started) {
501
+ alreadyStartedSources += 1;
502
+ continue;
503
+ }
504
+ try {
505
+ if (source.onStop) {
506
+ await source.onStop();
507
+ }
508
+ source.started = false;
509
+ startedSources += 1;
510
+ }
511
+ catch (error) {
512
+ failedSources.push({
513
+ outputId: source.outputId,
514
+ reason: error instanceof Error ? error.message : String(error),
515
+ });
516
+ }
517
+ }
518
+ return {
519
+ ok: failedSources.length === 0,
520
+ requestedAt,
521
+ matchedSources,
522
+ startedSources,
523
+ alreadyStartedSources,
524
+ failedSources,
525
+ };
526
+ }
527
+ getStatus() {
528
+ const sources = Array.from(this.sources.values()).map((source) => ({
529
+ outputId: source.outputId,
530
+ streamId: source.streamId,
531
+ media: source.media,
532
+ started: source.started,
533
+ frameCount: source.frameCount,
534
+ lastFrameAt: source.lastFrameAt,
535
+ }));
536
+ return {
537
+ ok: true,
538
+ endpoint: `http://127.0.0.1:${this.port}`,
539
+ sourceCount: sources.length,
540
+ startedSourceCount: sources.filter((source) => source.started).length,
541
+ clientCount: this.clients.size,
542
+ sources,
543
+ };
544
+ }
545
+ async ensureServer() {
546
+ if (this.server) {
547
+ return;
548
+ }
549
+ if (this.serverStarting) {
550
+ await this.serverStarting;
551
+ return;
552
+ }
553
+ const token = crypto.randomBytes(12).toString('hex');
554
+ const server = http.createServer((req, res) => {
555
+ const url = req.url ?? '/';
556
+ const method = req.method ?? 'GET';
557
+ if (method !== 'GET') {
558
+ res.writeHead(405);
559
+ res.end('method not allowed');
560
+ return;
561
+ }
562
+ if (url.startsWith('/preview/view')) {
563
+ const query = new URL(url, 'http://127.0.0.1').searchParams;
564
+ if (query.get('token') !== token) {
565
+ res.writeHead(403);
566
+ res.end('forbidden');
567
+ return;
568
+ }
569
+ const endpoint = `http://127.0.0.1:${this.port}`;
570
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
571
+ res.end(renderViewerHtml(endpoint, token));
572
+ return;
573
+ }
574
+ if (url.startsWith('/preview/events')) {
575
+ const query = new URL(url, 'http://127.0.0.1').searchParams;
576
+ if (query.get('token') !== token) {
577
+ res.writeHead(403);
578
+ res.end('forbidden');
579
+ return;
580
+ }
581
+ res.writeHead(200, {
582
+ 'content-type': 'text/event-stream',
583
+ 'cache-control': 'no-cache',
584
+ connection: 'keep-alive',
585
+ 'access-control-allow-origin': '*',
586
+ });
587
+ this.writeEvent(res, 'hello', { ok: true });
588
+ this.clients.add(res);
589
+ req.on('close', () => {
590
+ this.clients.delete(res);
591
+ if (this.clients.size === 0) {
592
+ void this.stopSources();
593
+ }
594
+ try {
595
+ res.end();
596
+ }
597
+ catch {
598
+ // no-op
599
+ }
600
+ });
601
+ return;
602
+ }
603
+ if (url.startsWith('/preview/start')) {
604
+ const query = new URL(url, 'http://127.0.0.1').searchParams;
605
+ if (query.get('token') !== token) {
606
+ res.writeHead(403);
607
+ res.end('forbidden');
608
+ return;
609
+ }
610
+ const filter = this.parseStreamFilter(query.get('streams'));
611
+ void this.startSources(filter).then((result) => {
612
+ const statusCode = result.ok ? 200 : 500;
613
+ res.writeHead(statusCode, { 'content-type': 'application/json; charset=utf-8' });
614
+ res.end(JSON.stringify(result));
615
+ }).catch((error) => {
616
+ res.writeHead(500, { 'content-type': 'application/json; charset=utf-8' });
617
+ res.end(JSON.stringify({
618
+ ok: false,
619
+ requestedAt: nowIsoString(),
620
+ reason: error instanceof Error ? error.message : String(error),
621
+ }));
622
+ });
623
+ return;
624
+ }
625
+ if (url.startsWith('/preview/stop')) {
626
+ const query = new URL(url, 'http://127.0.0.1').searchParams;
627
+ if (query.get('token') !== token) {
628
+ res.writeHead(403);
629
+ res.end('forbidden');
630
+ return;
631
+ }
632
+ const filter = this.parseStreamFilter(query.get('streams'));
633
+ void this.stopSources(filter).then((result) => {
634
+ const statusCode = result.ok ? 200 : 500;
635
+ res.writeHead(statusCode, { 'content-type': 'application/json; charset=utf-8' });
636
+ res.end(JSON.stringify(result));
637
+ }).catch((error) => {
638
+ res.writeHead(500, { 'content-type': 'application/json; charset=utf-8' });
639
+ res.end(JSON.stringify({
640
+ ok: false,
641
+ requestedAt: nowIsoString(),
642
+ reason: error instanceof Error ? error.message : String(error),
643
+ }));
644
+ });
645
+ return;
646
+ }
647
+ if (url.startsWith('/preview/status')) {
648
+ const query = new URL(url, 'http://127.0.0.1').searchParams;
649
+ if (query.get('token') !== token) {
650
+ res.writeHead(403);
651
+ res.end('forbidden');
652
+ return;
653
+ }
654
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
655
+ res.end(JSON.stringify(this.getStatus()));
656
+ return;
657
+ }
658
+ res.writeHead(404);
659
+ res.end('not found');
660
+ });
661
+ this.serverStarting = new Promise((resolve, reject) => {
662
+ const onError = (error) => {
663
+ server.off('listening', onListening);
664
+ reject(error);
665
+ };
666
+ const onListening = () => {
667
+ server.off('error', onError);
668
+ resolve();
669
+ };
670
+ server.once('error', onError);
671
+ server.once('listening', onListening);
672
+ server.listen(0, '127.0.0.1');
673
+ });
674
+ try {
675
+ await this.serverStarting;
676
+ const address = server.address();
677
+ if (!address || typeof address === 'string') {
678
+ throw new Error('preview gateway failed to bind localhost');
679
+ }
680
+ this.server = server;
681
+ this.token = token;
682
+ this.port = address.port;
683
+ this.pollTimer = setInterval(() => this.pollOnce(), POLL_MS);
684
+ }
685
+ catch (error) {
686
+ try {
687
+ server.close();
688
+ }
689
+ catch {
690
+ // no-op
691
+ }
692
+ throw error;
693
+ }
694
+ finally {
695
+ this.serverStarting = undefined;
696
+ }
697
+ }
698
+ async registerSource(outputId, media, streamId, filePath, onStart, onStop) {
699
+ await this.ensureServer();
700
+ this.sources.set(outputId, {
701
+ outputId,
702
+ media,
703
+ streamId,
704
+ path: filePath,
705
+ offset: 0,
706
+ buffer: Buffer.alloc(0),
707
+ started: false,
708
+ frameCount: 0,
709
+ onStart,
710
+ onStop,
711
+ });
712
+ return {
713
+ url: `http://127.0.0.1:${this.port}/preview/view?token=${this.token}`,
714
+ token: this.token,
715
+ endpoint: `http://127.0.0.1:${this.port}`,
716
+ };
717
+ }
718
+ unregisterSource(outputId) {
719
+ this.sources.delete(outputId);
720
+ if (this.sources.size === 0) {
721
+ this.dispose();
722
+ }
723
+ }
724
+ dispose() {
725
+ if (this.pollTimer) {
726
+ clearInterval(this.pollTimer);
727
+ this.pollTimer = undefined;
728
+ }
729
+ for (const client of this.clients) {
730
+ try {
731
+ client.end();
732
+ }
733
+ catch {
734
+ // no-op
735
+ }
736
+ }
737
+ this.clients.clear();
738
+ if (this.server) {
739
+ this.server.close();
740
+ this.server = undefined;
741
+ }
742
+ this.sources.clear();
743
+ this.token = '';
744
+ this.port = 0;
745
+ }
746
+ pollOnce() {
747
+ for (const source of this.sources.values()) {
748
+ if (!source.started) {
749
+ continue;
750
+ }
751
+ if (!fs.existsSync(source.path)) {
752
+ continue;
753
+ }
754
+ const stat = fs.statSync(source.path);
755
+ if (stat.size < source.offset) {
756
+ source.offset = 0;
757
+ source.buffer = Buffer.alloc(0);
758
+ }
759
+ if (stat.size === source.offset) {
760
+ continue;
761
+ }
762
+ const fd = fs.openSync(source.path, 'r');
763
+ try {
764
+ const chunk = Buffer.alloc(Number(stat.size - source.offset));
765
+ const bytesRead = fs.readSync(fd, chunk, 0, chunk.length, source.offset);
766
+ if (bytesRead <= 0) {
767
+ continue;
768
+ }
769
+ source.offset += bytesRead;
770
+ source.buffer = Buffer.concat([source.buffer, chunk.subarray(0, bytesRead)]);
771
+ }
772
+ finally {
773
+ fs.closeSync(fd);
774
+ }
775
+ while (source.buffer.length >= HEADER_SIZE) {
776
+ const header = parseHeader(source.buffer);
777
+ if (!header) {
778
+ source.buffer = source.buffer.subarray(1);
779
+ continue;
780
+ }
781
+ const total = HEADER_SIZE + header.payloadLength;
782
+ if (source.buffer.length < total) {
783
+ break;
784
+ }
785
+ const payload = source.buffer.subarray(HEADER_SIZE, total);
786
+ source.buffer = source.buffer.subarray(total);
787
+ const eventPayload = {
788
+ at: nowIsoString(),
789
+ media: normalizePreviewMedia(header.kind, source.media),
790
+ streamId: source.streamId,
791
+ timestampMs: header.timestampMs,
792
+ keyFrame: (header.flags & 0x1) === 0x1,
793
+ payloadBase64: payload.toString('base64'),
794
+ };
795
+ source.frameCount += 1;
796
+ source.lastFrameAt = String(eventPayload.at);
797
+ this.broadcast('frame', eventPayload);
798
+ }
799
+ }
800
+ }
801
+ writeEvent(client, eventName, data) {
802
+ client.write(`event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`);
803
+ }
804
+ broadcast(eventName, data) {
805
+ for (const client of this.clients) {
806
+ try {
807
+ this.writeEvent(client, eventName, data);
808
+ }
809
+ catch {
810
+ this.clients.delete(client);
811
+ }
812
+ }
813
+ }
814
+ }
815
+ exports.WebPreviewGateway = WebPreviewGateway;