streamer-node 1.0.31 → 1.0.37

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 (123) hide show
  1. package/dist/package.json +6 -2
  2. package/dist/scripts/README.md +670 -0
  3. package/dist/scripts/install-streamer-node.sh +117 -0
  4. package/dist/scripts/reframe_ball.py +354 -0
  5. package/dist/src/application/interfaces/StartStreamUseCase.types.d.ts +3 -1
  6. package/dist/src/application/interfaces/StartStreamUseCase.types.d.ts.map +1 -1
  7. package/dist/src/application/interfaces/StartStreamUseCase.types.js.map +1 -1
  8. package/dist/src/application/services/HttpClient.d.ts +21 -0
  9. package/dist/src/application/services/HttpClient.d.ts.map +1 -1
  10. package/dist/src/application/services/HttpClient.js +37 -5
  11. package/dist/src/application/services/HttpClient.js.map +1 -1
  12. package/dist/src/application/services/StreamManagerService.d.ts.map +1 -1
  13. package/dist/src/application/services/StreamManagerService.js +2 -0
  14. package/dist/src/application/services/StreamManagerService.js.map +1 -1
  15. package/dist/src/application/use-cases/CaptureHighlightUseCase.d.ts +52 -0
  16. package/dist/src/application/use-cases/CaptureHighlightUseCase.d.ts.map +1 -0
  17. package/dist/src/application/use-cases/CaptureHighlightUseCase.js +169 -0
  18. package/dist/src/application/use-cases/CaptureHighlightUseCase.js.map +1 -0
  19. package/dist/src/application/use-cases/StartStreamUseCase.d.ts +9 -1
  20. package/dist/src/application/use-cases/StartStreamUseCase.d.ts.map +1 -1
  21. package/dist/src/application/use-cases/StartStreamUseCase.js +125 -2
  22. package/dist/src/application/use-cases/StartStreamUseCase.js.map +1 -1
  23. package/dist/src/application/use-cases/StopStreamUseCase.d.ts +5 -1
  24. package/dist/src/application/use-cases/StopStreamUseCase.d.ts.map +1 -1
  25. package/dist/src/application/use-cases/StopStreamUseCase.js +7 -1
  26. package/dist/src/application/use-cases/StopStreamUseCase.js.map +1 -1
  27. package/dist/src/domain/events/StreamEvent.d.ts +5 -0
  28. package/dist/src/domain/events/StreamEvent.d.ts.map +1 -1
  29. package/dist/src/domain/services/BallReframer.d.ts +35 -0
  30. package/dist/src/domain/services/BallReframer.d.ts.map +1 -0
  31. package/dist/src/domain/services/BallReframer.js +3 -0
  32. package/dist/src/domain/services/BallReframer.js.map +1 -0
  33. package/dist/src/domain/services/FFmpegService.d.ts +6 -2
  34. package/dist/src/domain/services/FFmpegService.d.ts.map +1 -1
  35. package/dist/src/domain/services/HighlightSignalSource.d.ts +113 -0
  36. package/dist/src/domain/services/HighlightSignalSource.d.ts.map +1 -0
  37. package/dist/src/domain/services/HighlightSignalSource.js +3 -0
  38. package/dist/src/domain/services/HighlightSignalSource.js.map +1 -0
  39. package/dist/src/domain/services/SSEService.d.ts +1 -0
  40. package/dist/src/domain/services/SSEService.d.ts.map +1 -1
  41. package/dist/src/infrastructure/config/Config.d.ts +32 -0
  42. package/dist/src/infrastructure/config/Config.d.ts.map +1 -1
  43. package/dist/src/infrastructure/config/Config.js +60 -0
  44. package/dist/src/infrastructure/config/Config.js.map +1 -1
  45. package/dist/src/infrastructure/listeners/SupabaseListener.d.ts +5 -0
  46. package/dist/src/infrastructure/listeners/SupabaseListener.d.ts.map +1 -1
  47. package/dist/src/infrastructure/listeners/SupabaseListener.js +43 -5
  48. package/dist/src/infrastructure/listeners/SupabaseListener.js.map +1 -1
  49. package/dist/src/infrastructure/logging/LogShippingService.d.ts +3 -1
  50. package/dist/src/infrastructure/logging/LogShippingService.d.ts.map +1 -1
  51. package/dist/src/infrastructure/logging/LogShippingService.js +13 -4
  52. package/dist/src/infrastructure/logging/LogShippingService.js.map +1 -1
  53. package/dist/src/infrastructure/logging/RemoteLogger.d.ts +1 -0
  54. package/dist/src/infrastructure/logging/RemoteLogger.d.ts.map +1 -1
  55. package/dist/src/infrastructure/logging/RemoteLogger.js +1 -1
  56. package/dist/src/infrastructure/logging/RemoteLogger.js.map +1 -1
  57. package/dist/src/infrastructure/services/AdDownloaderService.d.ts +85 -0
  58. package/dist/src/infrastructure/services/AdDownloaderService.d.ts.map +1 -0
  59. package/dist/src/infrastructure/services/AdDownloaderService.js +530 -0
  60. package/dist/src/infrastructure/services/AdDownloaderService.js.map +1 -0
  61. package/dist/src/infrastructure/services/AdRotator.d.ts +63 -0
  62. package/dist/src/infrastructure/services/AdRotator.d.ts.map +1 -0
  63. package/dist/src/infrastructure/services/AdRotator.js +223 -0
  64. package/dist/src/infrastructure/services/AdRotator.js.map +1 -0
  65. package/dist/src/infrastructure/services/HighlightBufferManager.d.ts +65 -0
  66. package/dist/src/infrastructure/services/HighlightBufferManager.d.ts.map +1 -0
  67. package/dist/src/infrastructure/services/HighlightBufferManager.js +203 -0
  68. package/dist/src/infrastructure/services/HighlightBufferManager.js.map +1 -0
  69. package/dist/src/infrastructure/services/HighlightBufferRegistry.d.ts +23 -0
  70. package/dist/src/infrastructure/services/HighlightBufferRegistry.d.ts.map +1 -0
  71. package/dist/src/infrastructure/services/HighlightBufferRegistry.js +44 -0
  72. package/dist/src/infrastructure/services/HighlightBufferRegistry.js.map +1 -0
  73. package/dist/src/infrastructure/services/HighlightExtractorService.d.ts +65 -0
  74. package/dist/src/infrastructure/services/HighlightExtractorService.d.ts.map +1 -0
  75. package/dist/src/infrastructure/services/HighlightExtractorService.js +264 -0
  76. package/dist/src/infrastructure/services/HighlightExtractorService.js.map +1 -0
  77. package/dist/src/infrastructure/services/HighlightRendererService.d.ts +38 -0
  78. package/dist/src/infrastructure/services/HighlightRendererService.d.ts.map +1 -0
  79. package/dist/src/infrastructure/services/HighlightRendererService.js +182 -0
  80. package/dist/src/infrastructure/services/HighlightRendererService.js.map +1 -0
  81. package/dist/src/infrastructure/services/NodeFFmpegService.d.ts +6 -4
  82. package/dist/src/infrastructure/services/NodeFFmpegService.d.ts.map +1 -1
  83. package/dist/src/infrastructure/services/NodeFFmpegService.js +154 -32
  84. package/dist/src/infrastructure/services/NodeFFmpegService.js.map +1 -1
  85. package/dist/src/infrastructure/services/NodeSSEService.d.ts +8 -0
  86. package/dist/src/infrastructure/services/NodeSSEService.d.ts.map +1 -1
  87. package/dist/src/infrastructure/services/NodeSSEService.js +71 -27
  88. package/dist/src/infrastructure/services/NodeSSEService.js.map +1 -1
  89. package/dist/src/infrastructure/services/NullBallReframer.d.ts +11 -0
  90. package/dist/src/infrastructure/services/NullBallReframer.d.ts.map +1 -0
  91. package/dist/src/infrastructure/services/NullBallReframer.js +16 -0
  92. package/dist/src/infrastructure/services/NullBallReframer.js.map +1 -0
  93. package/dist/src/infrastructure/services/PythonBallReframer.d.ts +35 -0
  94. package/dist/src/infrastructure/services/PythonBallReframer.d.ts.map +1 -0
  95. package/dist/src/infrastructure/services/PythonBallReframer.js +238 -0
  96. package/dist/src/infrastructure/services/PythonBallReframer.js.map +1 -0
  97. package/dist/src/infrastructure/services/SerialHighlightListener.d.ts +45 -0
  98. package/dist/src/infrastructure/services/SerialHighlightListener.d.ts.map +1 -0
  99. package/dist/src/infrastructure/services/SerialHighlightListener.js +440 -0
  100. package/dist/src/infrastructure/services/SerialHighlightListener.js.map +1 -0
  101. package/dist/src/infrastructure/services/YouTubeUploadService.d.ts +36 -0
  102. package/dist/src/infrastructure/services/YouTubeUploadService.d.ts.map +1 -0
  103. package/dist/src/infrastructure/services/YouTubeUploadService.js +402 -0
  104. package/dist/src/infrastructure/services/YouTubeUploadService.js.map +1 -0
  105. package/dist/src/infrastructure/utils/ResourceSampler.d.ts +47 -0
  106. package/dist/src/infrastructure/utils/ResourceSampler.d.ts.map +1 -0
  107. package/dist/src/infrastructure/utils/ResourceSampler.js +102 -0
  108. package/dist/src/infrastructure/utils/ResourceSampler.js.map +1 -0
  109. package/dist/src/infrastructure/utils/lowPriority.d.ts +29 -0
  110. package/dist/src/infrastructure/utils/lowPriority.d.ts.map +1 -0
  111. package/dist/src/infrastructure/utils/lowPriority.js +31 -0
  112. package/dist/src/infrastructure/utils/lowPriority.js.map +1 -0
  113. package/dist/src/infrastructure/utils/paths.d.ts +27 -0
  114. package/dist/src/infrastructure/utils/paths.d.ts.map +1 -0
  115. package/dist/src/infrastructure/utils/paths.js +69 -0
  116. package/dist/src/infrastructure/utils/paths.js.map +1 -0
  117. package/dist/src/infrastructure/utils/spawnToFile.d.ts +20 -0
  118. package/dist/src/infrastructure/utils/spawnToFile.d.ts.map +1 -0
  119. package/dist/src/infrastructure/utils/spawnToFile.js +98 -0
  120. package/dist/src/infrastructure/utils/spawnToFile.js.map +1 -0
  121. package/dist/src/main.js +228 -2
  122. package/dist/src/main.js.map +1 -1
  123. package/package.json +6 -2
@@ -0,0 +1,440 @@
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.SerialHighlightListener = void 0;
37
+ const events_1 = require("events");
38
+ const fs = __importStar(require("fs"));
39
+ /**
40
+ * Reads the ESP32's USB-serial output to detect whether the highlight hardware
41
+ * is attached and alive (presence detection). Presence drives automatic
42
+ * enablement of the highlight buffer — see HighlightSignalSource.
43
+ *
44
+ * ── IN SIMPLE WORDS ──
45
+ * The court ESP32 plugs into the box over USB and prints one JSON object per
46
+ * line — a heartbeat every 5s, score updates, logs, and a `button` packet when
47
+ * someone presses the physical highlight button. This class keeps the serial
48
+ * port open and notes the last time it heard a valid line. If we've heard the
49
+ * device recently, it's "present" and the box should record highlights; if the
50
+ * box has no ESP32, we never hear it and `isDevicePresent()` stays false.
51
+ *
52
+ * Protocol contract: docs/esp32/STREAMER_INTEGRATION.md (authoritative — the
53
+ * desk harness in lib/esp-serial-com is NOT the spec; it only pretty-prints
54
+ * three of the four types).
55
+ *
56
+ * ── BUSINESS RULES ──
57
+ * - Highlight capture is enabled per-box by HARDWARE PRESENCE (decision
58
+ * 2026-07-18): a box with the ESP32 attached records the buffer; one without
59
+ * never does. `isDevicePresent()` is that signal.
60
+ * - Presence is proven by the ESP32's own heartbeat, emitted every 5s
61
+ * unconditionally (needs no mesh, no court). Freshness window = 15s
62
+ * (tolerate 3 missed beats).
63
+ * - A highlight is a `{"type":"button","event":"press"}` packet. It comes from
64
+ * a SEPARATE ESP8266 to the scoreboard one, on its own power and mesh nodeId,
65
+ * provisioned with the same courtId — that shared court ID is what correlates
66
+ * a press with the court it happened on.
67
+ * - COURT FILTERING IS MANDATORY. All units share one set of mesh credentials,
68
+ * so at a venue with two courts in WiFi range this ESP32 relays the other
69
+ * court's traffic verbatim (the firmware does no filtering — by design; the
70
+ * spec puts it on the streamer). The courtId rides on HighlightSignal and is
71
+ * matched downstream before any clip is cut.
72
+ *
73
+ * ── WHY IT'S BUILT THIS WAY (change at your peril) ──
74
+ * - Presence is CONTENT-based (a recognized packet seen recently), not merely
75
+ * port-open-based: an open port proves nothing about what's on the other end,
76
+ * so trusting it would let an unrelated serial gadget switch on the buffer.
77
+ * Content-gating means a wrong/other device reads as absent (no false
78
+ * positive) and we don't need the ESP32's USB vendor/product id (unconfirmed)
79
+ * to identify it. The ONE bounded exception is a short post-open grace (see
80
+ * OPEN_GRACE_MS) that bridges the gap before the first heartbeat on a fresh
81
+ * boot; it expires quickly and then content-gating governs.
82
+ * - The native `serialport` module is loaded defensively (lazy require in a
83
+ * try/catch). If it can't load on a given box, highlights just stay off
84
+ * forever — the live streaming app must never fail to boot over this.
85
+ *
86
+ * ── DO NOT ──
87
+ * - Do NOT make `isDevicePresent()` throw or do I/O — it's on the stream-start
88
+ * hot path. It is a couple of timestamp comparisons.
89
+ * - Do NOT treat port-open as *durable* presence (only the bounded grace).
90
+ * - Do NOT let a serial error/disconnect crash the process — port AND parser
91
+ * errors are handled and route to a backoff reconnect (mirrors NodeSSEService).
92
+ */
93
+ // The ESP32 heartbeats every 5s (esp32-leader.ino). Allow ~3 missed beats
94
+ // before declaring the device gone, so a single dropped line doesn't flap.
95
+ const PRESENCE_FRESHNESS_MS = 15000;
96
+ // Bounded startup grace: a freshly-opened matched device counts as present for
97
+ // just over one heartbeat interval, to bridge the window before the first
98
+ // heartbeat on a cold boot. After this, only real traffic keeps it present.
99
+ const OPEN_GRACE_MS = 7000;
100
+ // Reconnect backoff bounds (mirror NodeSSEService.scheduleRetry).
101
+ const RECONNECT_BASE_MS = 1000;
102
+ const RECONNECT_MAX_MS = 30000;
103
+ class SerialHighlightListener extends events_1.EventEmitter {
104
+ constructor(portPath, // "auto" or an explicit device path
105
+ baudRate, logger,
106
+ // Debug/test only (default off): force presence true, and/or fire a
107
+ // highlight whenever `triggerFile` appears on disk — so the pipeline can be
108
+ // exercised on a box without the ESP32 button hardware.
109
+ forcePresent = false, triggerFile = "") {
110
+ super();
111
+ this.portPath = portPath;
112
+ this.baudRate = baudRate;
113
+ this.logger = logger;
114
+ this.forcePresent = forcePresent;
115
+ this.triggerFile = triggerFile;
116
+ this.port = null;
117
+ this.parser = null;
118
+ this.lastTrafficAtMs = 0;
119
+ this.portOpenedAtMs = 0;
120
+ this.started = false;
121
+ this.reconnectTimer = null;
122
+ this.reconnectAttempts = 0;
123
+ this.triggerTimer = null;
124
+ // Native serial libs, lazily required in start(). Null when unavailable, in
125
+ // which case the listener is inert and isDevicePresent() stays false.
126
+ this.serialLib = null;
127
+ this.parserLib = null;
128
+ }
129
+ onHighlight(listener) {
130
+ this.on("highlight", listener);
131
+ }
132
+ onScore(listener) {
133
+ this.on("score", listener);
134
+ }
135
+ start() {
136
+ if (this.started)
137
+ return;
138
+ this.started = true;
139
+ this.startTriggerFileWatch();
140
+ try {
141
+ this.serialLib = require("serialport");
142
+ this.parserLib = require("@serialport/parser-readline");
143
+ }
144
+ catch (error) {
145
+ this.logger.warn("serialport module unavailable; highlight hardware detection disabled", { error: error instanceof Error ? error.message : String(error) });
146
+ return;
147
+ }
148
+ void this.openPort();
149
+ }
150
+ stop() {
151
+ this.started = false;
152
+ if (this.reconnectTimer) {
153
+ clearTimeout(this.reconnectTimer);
154
+ this.reconnectTimer = null;
155
+ }
156
+ if (this.triggerTimer) {
157
+ clearInterval(this.triggerTimer);
158
+ this.triggerTimer = null;
159
+ }
160
+ this.teardownPort();
161
+ this.lastTrafficAtMs = 0;
162
+ }
163
+ // Debug/test: when a trigger file path is configured, fire a highlight
164
+ // whenever that file appears (then remove it). Lets staging exercise the
165
+ // capture pipeline via `touch <file>` without the ESP32 button. No-op unless
166
+ // HIGHLIGHT_TRIGGER_FILE is set.
167
+ startTriggerFileWatch() {
168
+ if (!this.triggerFile)
169
+ return;
170
+ this.logger.info("Highlight manual trigger file watch enabled", {
171
+ triggerFile: this.triggerFile,
172
+ });
173
+ this.triggerTimer = setInterval(() => {
174
+ try {
175
+ if (!fs.existsSync(this.triggerFile))
176
+ return;
177
+ fs.unlinkSync(this.triggerFile);
178
+ this.logger.info("Highlight signal received (manual trigger file)");
179
+ this.emit("highlight", { receivedAtMs: Date.now() });
180
+ }
181
+ catch {
182
+ /* transient fs error; retry next tick */
183
+ }
184
+ }, 1000);
185
+ }
186
+ isDevicePresent() {
187
+ if (this.forcePresent)
188
+ return true; // debug/test override
189
+ const now = Date.now();
190
+ // Durable presence: recognized traffic within the freshness window.
191
+ if (now - this.lastTrafficAtMs <= PRESENCE_FRESHNESS_MS)
192
+ return true;
193
+ // Bounded startup grace: matched device just opened, first heartbeat not
194
+ // yet due. Expires into content-gating.
195
+ if (this.portOpenedAtMs && now - this.portOpenedAtMs <= OPEN_GRACE_MS) {
196
+ return true;
197
+ }
198
+ return false;
199
+ }
200
+ async openPort() {
201
+ if (!this.started || !this.serialLib || !this.parserLib)
202
+ return;
203
+ // Clear any lingering port/listeners before (re)opening so reconnects never
204
+ // accumulate orphaned SerialPort/parser objects and their listeners.
205
+ this.teardownPort();
206
+ let path = "";
207
+ try {
208
+ path = await this.resolvePortPath();
209
+ }
210
+ catch (error) {
211
+ this.scheduleReconnect(`serial discovery failed: ${error instanceof Error ? error.message : String(error)}`);
212
+ return;
213
+ }
214
+ if (!path) {
215
+ this.scheduleReconnect("no candidate serial device found");
216
+ return;
217
+ }
218
+ try {
219
+ const { SerialPort } = this.serialLib;
220
+ const { ReadlineParser } = this.parserLib;
221
+ const port = new SerialPort({
222
+ path,
223
+ baudRate: this.baudRate,
224
+ autoOpen: false,
225
+ });
226
+ port.open((err) => {
227
+ if (err) {
228
+ this.scheduleReconnect(`open ${path} failed: ${err.message}`);
229
+ return;
230
+ }
231
+ this.reconnectAttempts = 0;
232
+ this.portOpenedAtMs = Date.now(); // start the bounded presence grace
233
+ this.logger.info("Highlight serial port opened", { path });
234
+ });
235
+ // ESP32 uses println (\r\n); split on \n and trim the trailing \r.
236
+ const parser = port.pipe(new ReadlineParser({ delimiter: "\n" }));
237
+ parser.on("data", (line) => this.handleLine(String(line)));
238
+ // A parser-side stream error would otherwise be an unhandled 'error'
239
+ // event and crash the process — route it to the same reconnect path.
240
+ parser.on("error", (err) => {
241
+ this.logger.warn("Highlight serial parser error", {
242
+ error: err.message,
243
+ });
244
+ this.handlePortDown();
245
+ });
246
+ port.on("error", (err) => {
247
+ this.logger.warn("Highlight serial port error", { error: err.message });
248
+ this.handlePortDown();
249
+ });
250
+ port.on("close", () => this.handlePortDown());
251
+ this.port = port;
252
+ this.parser = parser;
253
+ }
254
+ catch (error) {
255
+ this.scheduleReconnect(`serial open threw: ${error instanceof Error ? error.message : String(error)}`);
256
+ }
257
+ }
258
+ handleLine(raw) {
259
+ // Trailing \r: the ESP32 uses Serial.println (CRLF) while we split on \n.
260
+ const line = raw.trim();
261
+ if (!line)
262
+ return;
263
+ // NOT every line is JSON. esp32-leader relays mesh messages it can't parse
264
+ // verbatim rather than dropping them, and boot-time chatter appears on the
265
+ // port too. A parse failure means "ignore this line" — never an error path,
266
+ // or the first line of ESP8266 boot noise takes the streamer down.
267
+ let msg;
268
+ try {
269
+ const parsed = JSON.parse(line);
270
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
271
+ return;
272
+ msg = parsed;
273
+ }
274
+ catch {
275
+ return;
276
+ }
277
+ const type = msg.type;
278
+ if (typeof type !== "string")
279
+ return;
280
+ // PRESENCE: any line that parsed as JSON and carries a string `type` proves
281
+ // the USB link is alive. Deliberately NOT gated on courtId — the ESP32's own
282
+ // heartbeat is {"type":"heartbeat","source":"ESP32"} with no court (it is the
283
+ // local leader), so requiring one would leave presence false forever.
284
+ const now = Date.now();
285
+ const wasFresh = now - this.lastTrafficAtMs <= PRESENCE_FRESHNESS_MS;
286
+ this.lastTrafficAtMs = now;
287
+ this.reconnectAttempts = 0;
288
+ if (!wasFresh) {
289
+ this.logger.info("Highlight hardware detected (ESP32 serial traffic)", {
290
+ type,
291
+ });
292
+ }
293
+ // Switch on `type` and IGNORE unknown types — the protocol reserves the
294
+ // right to add more. Four exist today: score, button, heartbeat, log.
295
+ if (type === "score") {
296
+ this.handleScore(msg);
297
+ return;
298
+ }
299
+ if (type !== "button")
300
+ return;
301
+ const courtId = typeof msg.courtId === "string" ? msg.courtId : undefined;
302
+ const seq = typeof msg.seq === "number" ? msg.seq : undefined;
303
+ // Press edge only, debounced 50ms in firmware — one physical click is one
304
+ // message, so no debounce of our own is needed.
305
+ if (typeof msg.event === "string" && msg.event !== "press")
306
+ return;
307
+ // `seq` is monotonic per boot, +1 per press. A gap means the mesh broadcast
308
+ // was lost (best-effort, no retry); going backwards means the node rebooted.
309
+ // Log it as a delivery signal — do not try to recover missed presses.
310
+ if (seq !== undefined && this.lastButtonSeq !== undefined) {
311
+ if (seq > this.lastButtonSeq + 1) {
312
+ this.logger.warn("Highlight button presses lost in mesh transit", {
313
+ courtId,
314
+ expected: this.lastButtonSeq + 1,
315
+ got: seq,
316
+ missed: seq - this.lastButtonSeq - 1,
317
+ });
318
+ }
319
+ else if (seq < this.lastButtonSeq) {
320
+ this.logger.info("Highlight button node rebooted (seq reset)", {
321
+ courtId,
322
+ previous: this.lastButtonSeq,
323
+ got: seq,
324
+ });
325
+ }
326
+ }
327
+ if (seq !== undefined)
328
+ this.lastButtonSeq = seq;
329
+ this.logger.info("Highlight signal received (button press)", {
330
+ courtId,
331
+ seq,
332
+ });
333
+ this.emit("highlight", { receivedAtMs: now, courtId });
334
+ }
335
+ /**
336
+ * A `score` packet: the board's current state, forwarded verbatim.
337
+ *
338
+ * Score packets are the one type that carries NO `source` field, so anything
339
+ * keying on `source` being present would drop them — switch on `type` only.
340
+ * Values are passed through untouched: interpreting "AD" or mapping the mode
341
+ * is the consumer's job, not the transport's.
342
+ */
343
+ handleScore(msg) {
344
+ const courtId = typeof msg.courtId === "string" ? msg.courtId : undefined;
345
+ // Without a court we cannot address the write, and at a multi-court venue we
346
+ // could not tell whose score it is — drop rather than guess.
347
+ if (!courtId)
348
+ return;
349
+ const scoreA = msg.scoreA;
350
+ const scoreB = msg.scoreB;
351
+ if (scoreA === undefined || scoreB === undefined)
352
+ return;
353
+ this.emit("score", {
354
+ courtId,
355
+ mode: typeof msg.mode === "string" ? msg.mode : undefined,
356
+ // Coerced with String() rather than assumed: the firmware sends strings,
357
+ // but a number here must not become "undefined" downstream.
358
+ scoreA: String(scoreA),
359
+ scoreB: String(scoreB),
360
+ gamesA: typeof msg.gamesA === "number" ? msg.gamesA : undefined,
361
+ gamesB: typeof msg.gamesB === "number" ? msg.gamesB : undefined,
362
+ });
363
+ }
364
+ async resolvePortPath() {
365
+ if (this.portPath && this.portPath !== "auto") {
366
+ return this.portPath;
367
+ }
368
+ // Auto-discovery: open ONLY an ESP32-looking device. Deliberately no "first
369
+ // available port" fallback — grabbing an arbitrary tty (e.g. a system debug
370
+ // console) could disturb unrelated hardware. If nothing matches, return ""
371
+ // so openPort() backs off and re-scans later (the ESP32 may be plugged in
372
+ // after boot).
373
+ //
374
+ // Match on MANUFACTURER, not product id: the boards ship with either a
375
+ // CP2102 (Silicon Labs) or a CH340 (wch.cn / QinHeng) bridge, so no single
376
+ // PID covers the fleet. Path matching alone is not enough — on a box that
377
+ // also has the ESP8266 debug cable or an Arduino FTDI attached, "first
378
+ // USB-ish tty" can open the wrong device and then sit there hearing
379
+ // nothing. Manufacturer is checked first for that reason.
380
+ const { SerialPort } = this.serialLib;
381
+ const ports = await SerialPort.list();
382
+ const isEsp32 = (p) => {
383
+ const mfr = p.manufacturer ?? "";
384
+ if (/silicon labs|wch\.cn|qinheng/i.test(mfr))
385
+ return true;
386
+ // Linux reports the bare USB vendor ID for the CH340 instead of a name.
387
+ if (p.vendorId?.toLowerCase() === "1a86")
388
+ return true;
389
+ return /usbserial|ttyusb|ttyacm|cu\.usb|tty\.usb/i.test(p.path);
390
+ };
391
+ // Prefer a manufacturer/vendor match over a bare path match.
392
+ const byIdentity = ports.find((p) => /silicon labs|wch\.cn|qinheng/i.test(p.manufacturer ?? "") ||
393
+ p.vendorId?.toLowerCase() === "1a86");
394
+ return (byIdentity ?? ports.find(isEsp32))?.path ?? "";
395
+ }
396
+ handlePortDown() {
397
+ this.teardownPort();
398
+ this.scheduleReconnect("serial port closed/errored");
399
+ }
400
+ // Remove listeners and close the current port/parser, resetting the grace.
401
+ // Idempotent and never throws.
402
+ teardownPort() {
403
+ if (this.parser) {
404
+ try {
405
+ this.parser.removeAllListeners();
406
+ }
407
+ catch {
408
+ /* ignore */
409
+ }
410
+ this.parser = null;
411
+ }
412
+ if (this.port) {
413
+ try {
414
+ this.port.removeAllListeners();
415
+ this.port.close(() => { });
416
+ }
417
+ catch {
418
+ /* already closing/closed */
419
+ }
420
+ this.port = null;
421
+ }
422
+ this.portOpenedAtMs = 0;
423
+ }
424
+ scheduleReconnect(reason) {
425
+ if (!this.started || this.reconnectTimer)
426
+ return;
427
+ this.reconnectAttempts = Math.min(this.reconnectAttempts + 1, 10);
428
+ const delay = Math.min(RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempts - 1), RECONNECT_MAX_MS);
429
+ this.logger.warn("Highlight serial reconnect scheduled", {
430
+ reason,
431
+ delayMs: delay,
432
+ });
433
+ this.reconnectTimer = setTimeout(() => {
434
+ this.reconnectTimer = null;
435
+ void this.openPort();
436
+ }, delay);
437
+ }
438
+ }
439
+ exports.SerialHighlightListener = SerialHighlightListener;
440
+ //# sourceMappingURL=SerialHighlightListener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SerialHighlightListener.js","sourceRoot":"","sources":["../../../../src/infrastructure/services/SerialHighlightListener.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { Logger } from "../../application/interfaces/Logger";
2
+ export interface ReelUploadRequest {
3
+ filePath: string;
4
+ courtId: string;
5
+ streamKey: string;
6
+ title: string;
7
+ description?: string;
8
+ preferShort?: boolean;
9
+ }
10
+ export declare class YouTubeUploadService {
11
+ private readonly baseUrl;
12
+ private readonly groundId;
13
+ private readonly streamingApiKey;
14
+ private readonly logger;
15
+ constructor(baseUrl: string, groundId: string, streamingApiKey: string, logger: Logger);
16
+ /**
17
+ * Upload `filePath` for `courtId`. Returns the YouTube video id on success,
18
+ * or null on any failure (logged). Reports the outcome to the backend either
19
+ * way (so a device-reported failure is recorded).
20
+ */
21
+ upload(req: ReelUploadRequest): Promise<string | null>;
22
+ private createSession;
23
+ private putFile;
24
+ private reportComplete;
25
+ private readChunk;
26
+ private parseRangeEnd;
27
+ private isRatio;
28
+ private probeDimensions;
29
+ private probeDurationSec;
30
+ private fetchWithTimeout;
31
+ private safeText;
32
+ /** Elapsed time + average throughput, so a slow link is visible in the logs. */
33
+ private transferStats;
34
+ private delay;
35
+ }
36
+ //# sourceMappingURL=YouTubeUploadService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YouTubeUploadService.d.ts","sourceRoot":"","sources":["../../../../src/infrastructure/services/YouTubeUploadService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAC;AA4C7D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,qBAAa,oBAAoB;IAE7B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAHN,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,MAAM;IAGjC;;;;OAIG;IACU,MAAM,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;YAuGrD,aAAa;YA8Cb,OAAO;YAsFP,cAAc;IAmC5B,OAAO,CAAC,SAAS;IAajB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,eAAe;IAsBvB,OAAO,CAAC,gBAAgB;YAkBV,gBAAgB;YAchB,QAAQ;IAQtB,gFAAgF;IAChF,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,KAAK;CAGd"}