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,223 @@
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.AdRotationRegistry = exports.AdRotator = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ /**
39
+ * Rotates a pool of N normalized still ads through two on-screen slots
40
+ * (left/right) while the stream stays live. Each slot advances independently on
41
+ * its own timer; the two slots never show the same ad at once; the pool is
42
+ * reshuffled each time it is fully consumed.
43
+ *
44
+ * Mechanism: the running ffmpeg reads two fixed PNG files (the slot files) via
45
+ * `-f image2 -loop 1`, re-reading them on every loop. To change a slot we simply
46
+ * overwrite its file (atomically, via temp + rename). The ffmpeg process never
47
+ * restarts.
48
+ */
49
+ class AdRotator {
50
+ constructor(courtId, pool, slotLeftPath, slotRightPath, options, logger) {
51
+ this.courtId = courtId;
52
+ this.pool = pool;
53
+ this.slotLeftPath = slotLeftPath;
54
+ this.slotRightPath = slotRightPath;
55
+ this.options = options;
56
+ this.logger = logger;
57
+ this.queue = [];
58
+ this.cursor = 0;
59
+ this.left = { poolIndex: -1, timer: null };
60
+ this.right = { poolIndex: -1, timer: null };
61
+ this.stopped = false;
62
+ }
63
+ /**
64
+ * Seed the initial slot files. MUST run before ffmpeg starts so the slot
65
+ * inputs exist. Clears any stale slot files first, so an empty pool leaves no
66
+ * slot files behind (ffmpeg then adds no ad inputs).
67
+ */
68
+ prepare() {
69
+ this.removeSlotFiles();
70
+ if (this.pool.length === 0) {
71
+ return;
72
+ }
73
+ this.reshuffle();
74
+ this.cursor = 0;
75
+ this.left.poolIndex = this.next(-1);
76
+ this.writeSlot(this.slotLeftPath, this.pool[this.left.poolIndex]);
77
+ if (this.pool.length >= 2) {
78
+ this.right.poolIndex = this.next(this.left.poolIndex);
79
+ this.writeSlot(this.slotRightPath, this.pool[this.right.poolIndex]);
80
+ }
81
+ }
82
+ /**
83
+ * Begin rotation. Call after ffmpeg has started. With fewer than two ads there
84
+ * is nothing to rotate, so the seeded slot(s) simply stay put.
85
+ */
86
+ start() {
87
+ if (this.stopped || this.pool.length < 2) {
88
+ return;
89
+ }
90
+ this.logger.info("Starting ad rotation", {
91
+ courtId: this.courtId,
92
+ poolSize: this.pool.length,
93
+ });
94
+ // Left rotates after a full duration; right is offset by half its duration
95
+ // so the two slots never swap on the same tick.
96
+ this.scheduleSlot(this.left, this.slotLeftPath, this.right);
97
+ this.right.timer = setTimeout(() => {
98
+ if (this.stopped)
99
+ return;
100
+ this.right.poolIndex = this.next(this.left.poolIndex);
101
+ this.writeSlot(this.slotRightPath, this.pool[this.right.poolIndex]);
102
+ this.scheduleSlot(this.right, this.slotRightPath, this.left);
103
+ }, Math.floor(this.durationMs(this.right.poolIndex) / 2));
104
+ }
105
+ /** Stop all timers. Idempotent. Safe to call from teardown/retry/shutdown. */
106
+ stop() {
107
+ if (this.stopped)
108
+ return;
109
+ this.stopped = true;
110
+ if (this.left.timer)
111
+ clearTimeout(this.left.timer);
112
+ if (this.right.timer)
113
+ clearTimeout(this.right.timer);
114
+ this.left.timer = null;
115
+ this.right.timer = null;
116
+ this.logger.info("Stopped ad rotation", { courtId: this.courtId });
117
+ }
118
+ scheduleSlot(slot, slotPath, otherSlot) {
119
+ if (this.stopped)
120
+ return;
121
+ slot.timer = setTimeout(() => {
122
+ if (this.stopped)
123
+ return;
124
+ slot.poolIndex = this.next(otherSlot.poolIndex);
125
+ this.writeSlot(slotPath, this.pool[slot.poolIndex]);
126
+ this.scheduleSlot(slot, slotPath, otherSlot);
127
+ }, this.durationMs(slot.poolIndex));
128
+ }
129
+ // Advance the cursor and return the next pool index, skipping the ad currently
130
+ // shown in the other slot (so both slots never show the same ad). Reshuffles
131
+ // when a full cycle has been consumed.
132
+ next(skipPoolIndex) {
133
+ const maxAttempts = this.queue.length + 1;
134
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
135
+ if (this.cursor >= this.queue.length) {
136
+ this.reshuffle();
137
+ this.cursor = 0;
138
+ }
139
+ const idx = this.queue[this.cursor++];
140
+ // With a pool of <2 the skip cannot be honoured; allow the duplicate.
141
+ if (idx !== skipPoolIndex || this.pool.length < 2) {
142
+ return idx;
143
+ }
144
+ }
145
+ return (this.queue.find((i) => i !== skipPoolIndex) ?? this.queue[0]) ?? 0;
146
+ }
147
+ // Resolve the slot duration: payload value when valid, else the configured
148
+ // default, clamped to [min, max] to avoid file-swap thrash / runaway values.
149
+ durationMs(poolIndex) {
150
+ const raw = this.pool[poolIndex]?.durationSec;
151
+ let sec = typeof raw === "number" && isFinite(raw) && raw > 0
152
+ ? raw
153
+ : this.options.defaultDurationSec;
154
+ sec = Math.max(this.options.minDurationSec, Math.min(this.options.maxDurationSec, sec));
155
+ return sec * 1000;
156
+ }
157
+ // Atomically replace a slot file so ffmpeg never reads a half-written PNG.
158
+ writeSlot(slotPath, ad) {
159
+ try {
160
+ const tmp = `${slotPath}.tmp`;
161
+ fs.copyFileSync(ad.pngPath, tmp);
162
+ fs.renameSync(tmp, slotPath);
163
+ }
164
+ catch (error) {
165
+ this.logger.warn("Failed to write ad slot", {
166
+ courtId: this.courtId,
167
+ slotPath,
168
+ source: ad.pngPath,
169
+ error: error instanceof Error ? error.message : String(error),
170
+ });
171
+ }
172
+ }
173
+ removeSlotFiles() {
174
+ for (const p of [this.slotLeftPath, this.slotRightPath]) {
175
+ try {
176
+ if (fs.existsSync(p))
177
+ fs.unlinkSync(p);
178
+ }
179
+ catch {
180
+ // ignore — a stale slot file we can't remove will simply be overwritten
181
+ }
182
+ }
183
+ }
184
+ // Fisher–Yates shuffle of [0..pool.length-1].
185
+ reshuffle() {
186
+ const arr = Array.from({ length: this.pool.length }, (_, i) => i);
187
+ for (let i = arr.length - 1; i > 0; i--) {
188
+ const j = Math.floor(Math.random() * (i + 1));
189
+ [arr[i], arr[j]] = [arr[j], arr[i]];
190
+ }
191
+ this.queue = arr;
192
+ }
193
+ }
194
+ exports.AdRotator = AdRotator;
195
+ /**
196
+ * Tracks the live AdRotator per court so its timers can be torn down when the
197
+ * stream stops, retries, fails, or the app shuts down. A leaked rotator would
198
+ * keep overwriting slot files for a dead stream.
199
+ */
200
+ class AdRotationRegistry {
201
+ constructor() {
202
+ this.rotators = new Map();
203
+ }
204
+ set(courtId, rotator) {
205
+ this.stop(courtId);
206
+ this.rotators.set(courtId, rotator);
207
+ }
208
+ stop(courtId) {
209
+ const rotator = this.rotators.get(courtId);
210
+ if (rotator) {
211
+ rotator.stop();
212
+ this.rotators.delete(courtId);
213
+ }
214
+ }
215
+ stopAll() {
216
+ for (const rotator of this.rotators.values()) {
217
+ rotator.stop();
218
+ }
219
+ this.rotators.clear();
220
+ }
221
+ }
222
+ exports.AdRotationRegistry = AdRotationRegistry;
223
+ //# sourceMappingURL=AdRotator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AdRotator.js","sourceRoot":"","sources":["../../../../src/infrastructure/services/AdRotator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ import { Logger } from "../../application/interfaces/Logger";
2
+ /**
3
+ * Bookkeeping for one court's rolling highlight buffer: prunes aged segments
4
+ * and answers "which segment files cover this time window?".
5
+ *
6
+ * ── IN SIMPLE WORDS ──
7
+ * ffmpeg (the live process) writes short video chunks into a folder forever.
8
+ * This class is the janitor + index for that folder: every few seconds it
9
+ * deletes chunks older than the retention window (so the disk can't fill), and
10
+ * on demand it can list the chunks that overlap a requested time span (used
11
+ * later to cut a highlight clip). It runs NO process of its own — the recording
12
+ * is a second output of the live ffmpeg; this is pure filesystem bookkeeping.
13
+ *
14
+ * ── BUSINESS RULES ──
15
+ * - Retention: a chunk is kept only while newer than `retentionSec`. That value
16
+ * is sized (Config) to exceed the full highlight window (pre + post + lag),
17
+ * so pruning can never delete footage a pending extraction still needs.
18
+ * - A chunk's start time is read from its filename: the ffmpeg segment muxer
19
+ * writes `seg-<runToken>-<epochSeconds>.ts` (`-strftime 1`, `%s`), so the
20
+ * manifest is reconstructable from disk alone — no separate state to persist
21
+ * or keep in sync, and it survives a restart.
22
+ *
23
+ * ── WHY IT'S BUILT THIS WAY (change at your peril) ──
24
+ * - The sweep is best-effort and NEVER throws: a cleanup failure must not crash
25
+ * the app or disturb the live stream. Errors are logged and the next tick
26
+ * retries.
27
+ * - Segment END time is start + `segmentSec`; retention/overlap compare against
28
+ * END so a chunk is only dropped once its last frame is older than retention.
29
+ *
30
+ * ── DO NOT ──
31
+ * - Do NOT lower `retentionSec` below the extraction window, or a highlight
32
+ * fired near the retention edge will have its oldest needed chunk already
33
+ * deleted.
34
+ * - Do NOT assume `getSegmentsInWindow` returns a value — it returns null when
35
+ * the buffer lacks the requested leading history (callers must handle it).
36
+ */
37
+ export interface SegmentRecord {
38
+ path: string;
39
+ startMs: number;
40
+ endMs: number;
41
+ }
42
+ export declare class HighlightBufferManager {
43
+ private readonly courtId;
44
+ private readonly bufferDir;
45
+ private readonly segmentSec;
46
+ private readonly retentionSec;
47
+ private readonly logger;
48
+ private sweepTimer;
49
+ constructor(courtId: string, bufferDir: string, segmentSec: number, retentionSec: number, logger: Logger);
50
+ /** Begin the periodic retention sweep. Idempotent. */
51
+ start(): void;
52
+ /** Stop the sweep. Idempotent; safe from teardown/retry/shutdown. */
53
+ stop(): void;
54
+ /**
55
+ * Ordered segment records overlapping [startMs, endMs], or null when the
56
+ * buffer lacks the leading history to cover the window (e.g. the highlight
57
+ * fired before enough footage was recorded). "Covers the leading edge" means
58
+ * some segment starts at or before startMs.
59
+ */
60
+ getSegmentsInWindow(startMs: number, endMs: number): SegmentRecord[] | null;
61
+ private sweep;
62
+ private readSegments;
63
+ private parseSegmentFile;
64
+ }
65
+ //# sourceMappingURL=HighlightBufferManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HighlightBufferManager.d.ts","sourceRoot":"","sources":["../../../../src/infrastructure/services/HighlightBufferManager.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAYD,qBAAa,sBAAsB;IAI/B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAPzB,OAAO,CAAC,UAAU,CAA+B;gBAG9B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM;IAGjC,sDAAsD;IAC/C,KAAK,IAAI,IAAI;IAUpB,qEAAqE;IAC9D,IAAI,IAAI,IAAI;IAUnB;;;;;OAKG;IACI,mBAAmB,CACxB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,aAAa,EAAE,GAAG,IAAI;YAoDX,KAAK;IAsCnB,OAAO,CAAC,YAAY;IAmBpB,OAAO,CAAC,gBAAgB;CAWzB"}
@@ -0,0 +1,203 @@
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.HighlightBufferManager = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ // Cadence of the retention sweep. Frequent enough that on-disk data never
40
+ // exceeds retention by more than a few seconds; cheap (one readdir + a handful
41
+ // of unlinks over a ~tens-of-files directory).
42
+ const SWEEP_INTERVAL_MS = 5000;
43
+ // Pull the epoch-seconds start time out of `seg-<runToken>-<epoch>.ts`. The
44
+ // trailing `-(digits).ts` is always the timestamp (runToken may itself contain
45
+ // digits, but the epoch is the final hyphen-delimited numeric group).
46
+ const SEGMENT_EPOCH_RE = /-(\d+)\.ts$/;
47
+ class HighlightBufferManager {
48
+ constructor(courtId, bufferDir, segmentSec, retentionSec, logger) {
49
+ this.courtId = courtId;
50
+ this.bufferDir = bufferDir;
51
+ this.segmentSec = segmentSec;
52
+ this.retentionSec = retentionSec;
53
+ this.logger = logger;
54
+ this.sweepTimer = null;
55
+ }
56
+ /** Begin the periodic retention sweep. Idempotent. */
57
+ start() {
58
+ if (this.sweepTimer)
59
+ return;
60
+ this.logger.info("Highlight buffer manager started", {
61
+ courtId: this.courtId,
62
+ bufferDir: this.bufferDir,
63
+ retentionSec: this.retentionSec,
64
+ });
65
+ this.sweepTimer = setInterval(() => void this.sweep(), SWEEP_INTERVAL_MS);
66
+ }
67
+ /** Stop the sweep. Idempotent; safe from teardown/retry/shutdown. */
68
+ stop() {
69
+ if (this.sweepTimer) {
70
+ clearInterval(this.sweepTimer);
71
+ this.sweepTimer = null;
72
+ this.logger.info("Highlight buffer manager stopped", {
73
+ courtId: this.courtId,
74
+ });
75
+ }
76
+ }
77
+ /**
78
+ * Ordered segment records overlapping [startMs, endMs], or null when the
79
+ * buffer lacks the leading history to cover the window (e.g. the highlight
80
+ * fired before enough footage was recorded). "Covers the leading edge" means
81
+ * some segment starts at or before startMs.
82
+ */
83
+ getSegmentsInWindow(startMs, endMs) {
84
+ const all = this.readSegments();
85
+ if (all.length === 0)
86
+ return null;
87
+ const overlapping = all.filter((s) => s.endMs > startMs && s.startMs < endMs);
88
+ if (overlapping.length === 0)
89
+ return null;
90
+ // Leading edge must be present — we can never recover un-buffered past.
91
+ if (overlapping[0].startMs > startMs) {
92
+ this.logger.warn("Highlight window missing leading footage", {
93
+ courtId: this.courtId,
94
+ needFromMs: startMs,
95
+ haveFromMs: overlapping[0].startMs,
96
+ });
97
+ return null;
98
+ }
99
+ // Trailing edge must be present — otherwise the clip would be silently
100
+ // short (e.g. the post-roll segment hasn't flushed yet).
101
+ const last = overlapping[overlapping.length - 1];
102
+ if (last.endMs < endMs) {
103
+ this.logger.warn("Highlight window missing trailing footage", {
104
+ courtId: this.courtId,
105
+ needToMs: endMs,
106
+ haveToMs: last.endMs,
107
+ });
108
+ return null;
109
+ }
110
+ // Segments must be contiguous — a hole would shift all following content in
111
+ // the concatenated timeline, so the extracted window would capture the
112
+ // wrong moment. Tolerate sub-second rounding; flag any real gap.
113
+ const maxJoinGapMs = 1000;
114
+ for (let i = 1; i < overlapping.length; i++) {
115
+ if (overlapping[i].startMs - overlapping[i - 1].endMs > maxJoinGapMs) {
116
+ this.logger.warn("Highlight window has a segment gap; aborting", {
117
+ courtId: this.courtId,
118
+ gapAfterMs: overlapping[i - 1].endMs,
119
+ nextStartMs: overlapping[i].startMs,
120
+ });
121
+ return null;
122
+ }
123
+ }
124
+ return overlapping;
125
+ }
126
+ // Best-effort deletion of segments whose last frame is older than retention.
127
+ // Async I/O so the 5s timer never blocks the event loop. Never throws
128
+ // (fire-and-forget from setInterval).
129
+ async sweep() {
130
+ try {
131
+ const cutoffMs = Date.now() - this.retentionSec * 1000;
132
+ let files;
133
+ try {
134
+ files = await fs.promises.readdir(this.bufferDir);
135
+ }
136
+ catch {
137
+ return; // dir not created yet / transient
138
+ }
139
+ let removed = 0;
140
+ for (const file of files) {
141
+ const seg = this.parseSegmentFile(file);
142
+ if (seg && seg.endMs < cutoffMs) {
143
+ try {
144
+ await fs.promises.unlink(seg.path);
145
+ removed++;
146
+ }
147
+ catch {
148
+ /* file may already be gone; ignore */
149
+ }
150
+ }
151
+ }
152
+ if (removed > 0) {
153
+ this.logger.debug("Highlight buffer swept", {
154
+ courtId: this.courtId,
155
+ removed,
156
+ });
157
+ }
158
+ }
159
+ catch (error) {
160
+ this.logger.warn("Highlight buffer sweep failed (non-fatal)", {
161
+ courtId: this.courtId,
162
+ error: error instanceof Error ? error.message : String(error),
163
+ });
164
+ }
165
+ }
166
+ // Parse the buffer directory into sorted segment records from filenames.
167
+ // Returns [] when the directory doesn't exist yet or has no valid segments.
168
+ // Synchronous by design: called on-demand (window queries), not on a timer.
169
+ readSegments() {
170
+ let files;
171
+ try {
172
+ files = fs.readdirSync(this.bufferDir);
173
+ }
174
+ catch {
175
+ return []; // dir not created yet / transient
176
+ }
177
+ const records = [];
178
+ for (const file of files) {
179
+ const seg = this.parseSegmentFile(file);
180
+ if (seg)
181
+ records.push(seg);
182
+ }
183
+ records.sort((a, b) => a.startMs - b.startMs);
184
+ return records;
185
+ }
186
+ // Map a filename (`seg-<runToken>-<epoch>.ts`) to a record, or null if it
187
+ // isn't a recognizable segment. Shared by readSegments and sweep.
188
+ parseSegmentFile(file) {
189
+ const match = file.match(SEGMENT_EPOCH_RE);
190
+ if (!match)
191
+ return null;
192
+ const startMs = parseInt(match[1], 10) * 1000;
193
+ if (!Number.isFinite(startMs))
194
+ return null;
195
+ return {
196
+ path: path.join(this.bufferDir, file),
197
+ startMs,
198
+ endMs: startMs + this.segmentSec * 1000,
199
+ };
200
+ }
201
+ }
202
+ exports.HighlightBufferManager = HighlightBufferManager;
203
+ //# sourceMappingURL=HighlightBufferManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HighlightBufferManager.js","sourceRoot":"","sources":["../../../../src/infrastructure/services/HighlightBufferManager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { HighlightBufferManager } from "./HighlightBufferManager";
2
+ /**
3
+ * Tracks the live HighlightBufferManager per court so its retention sweep is
4
+ * started on stream start and reliably stopped on stop / retry / shutdown —
5
+ * mirroring AdRotationRegistry. Without this, a leaked sweep timer would keep
6
+ * pruning a directory for a stream that no longer exists.
7
+ *
8
+ * ── DO NOT ──
9
+ * - Do NOT leak: every code path that ends a stream (stop use-case, retry,
10
+ * killAll/shutdown) must call stop(courtId) or stopAll().
11
+ */
12
+ export declare class HighlightBufferRegistry {
13
+ private readonly managers;
14
+ /** Register a court's manager, replacing (and stopping) any prior one. */
15
+ set(courtId: string, manager: HighlightBufferManager): void;
16
+ /** Look up a court's manager (e.g. for window queries during extraction). */
17
+ get(courtId: string): HighlightBufferManager | undefined;
18
+ /** Stop and deregister one court's manager. Idempotent. */
19
+ stop(courtId: string): void;
20
+ /** Stop and deregister all managers (shutdown / killAll). */
21
+ stopAll(): void;
22
+ }
23
+ //# sourceMappingURL=HighlightBufferRegistry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HighlightBufferRegistry.d.ts","sourceRoot":"","sources":["../../../../src/infrastructure/services/HighlightBufferRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAElE;;;;;;;;;GASG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6C;IAEtE,0EAA0E;IAC1E,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB,GAAG,IAAI;IAK3D,6EAA6E;IAC7E,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,sBAAsB,GAAG,SAAS;IAIxD,2DAA2D;IAC3D,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAQ3B,6DAA6D;IAC7D,OAAO,IAAI,IAAI;CAMhB"}
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HighlightBufferRegistry = void 0;
4
+ /**
5
+ * Tracks the live HighlightBufferManager per court so its retention sweep is
6
+ * started on stream start and reliably stopped on stop / retry / shutdown —
7
+ * mirroring AdRotationRegistry. Without this, a leaked sweep timer would keep
8
+ * pruning a directory for a stream that no longer exists.
9
+ *
10
+ * ── DO NOT ──
11
+ * - Do NOT leak: every code path that ends a stream (stop use-case, retry,
12
+ * killAll/shutdown) must call stop(courtId) or stopAll().
13
+ */
14
+ class HighlightBufferRegistry {
15
+ constructor() {
16
+ this.managers = new Map();
17
+ }
18
+ /** Register a court's manager, replacing (and stopping) any prior one. */
19
+ set(courtId, manager) {
20
+ this.stop(courtId);
21
+ this.managers.set(courtId, manager);
22
+ }
23
+ /** Look up a court's manager (e.g. for window queries during extraction). */
24
+ get(courtId) {
25
+ return this.managers.get(courtId);
26
+ }
27
+ /** Stop and deregister one court's manager. Idempotent. */
28
+ stop(courtId) {
29
+ const manager = this.managers.get(courtId);
30
+ if (manager) {
31
+ manager.stop();
32
+ this.managers.delete(courtId);
33
+ }
34
+ }
35
+ /** Stop and deregister all managers (shutdown / killAll). */
36
+ stopAll() {
37
+ for (const manager of this.managers.values()) {
38
+ manager.stop();
39
+ }
40
+ this.managers.clear();
41
+ }
42
+ }
43
+ exports.HighlightBufferRegistry = HighlightBufferRegistry;
44
+ //# sourceMappingURL=HighlightBufferRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HighlightBufferRegistry.js","sourceRoot":"","sources":["../../../../src/infrastructure/services/HighlightBufferRegistry.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;GASG;AACH,MAAa,uBAAuB;IAApC;QACmB,aAAQ,GAAG,IAAI,GAAG,EAAkC,CAAC;IA6BxE,CAAC;IA3BC,0EAA0E;IAC1E,GAAG,CAAC,OAAe,EAAE,OAA+B;QAClD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACnB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACtC,CAAC;IAED,6EAA6E;IAC7E,GAAG,CAAC,OAAe;QACjB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC;IAED,2DAA2D;IAC3D,IAAI,CAAC,OAAe;QAClB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,EAAE,CAAC;YACf,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAED,6DAA6D;IAC7D,OAAO;QACL,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC7C,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;CACF;AA9BD,0DA8BC"}
@@ -0,0 +1,65 @@
1
+ import { Logger } from "../../application/interfaces/Logger";
2
+ import { SegmentRecord } from "./HighlightBufferManager";
3
+ /**
4
+ * Cuts a highlight clip out of the rolling buffer's segment files.
5
+ *
6
+ * ── IN SIMPLE WORDS ──
7
+ * Given the buffered video chunks that overlap a time window, this stitches
8
+ * them together and trims to exactly the window, producing one clip file. It
9
+ * runs a one-shot ffmpeg, completely separate from the live stream.
10
+ *
11
+ * ── BUSINESS RULES ──
12
+ * - The clip spans [windowStart, windowEnd]; the trim offset is measured from
13
+ * the FIRST overlapping segment's start (which sits at or before windowStart,
14
+ * guaranteed by the buffer manifest's leading-edge check).
15
+ * - The extracted clip is RAW (no logos/crop). Ball-tracking reframe + logo
16
+ * overlay (Phase 5) run on this clip afterwards; here we only get the window
17
+ * right.
18
+ *
19
+ * ── WHY IT'S BUILT THIS WAY (change at your peril) ──
20
+ * - Output-side `-ss`/`-t` (after `-i`) + a real re-encode gives a
21
+ * frame-accurate cut. Stream-copy would snap to the nearest keyframe (up to
22
+ * `bufferSegmentSec` off) — unacceptable for a tight highlight window.
23
+ * - temp-file + atomic rename: a killed/failed ffmpeg never leaves a corrupt
24
+ * clip at the final path. Fail-soft: any error returns null (logged), never
25
+ * throws into the caller — a failed highlight must not disrupt anything.
26
+ *
27
+ * - Segments are HARDLINKED into a private dir before ffmpeg reads them. The
28
+ * retention sweeper runs every 5s and this re-encode takes ~30s, so without
29
+ * pinning the sweeper deletes segments ffmpeg hasn't reached yet — and
30
+ * ffmpeg then TRUNCATES THE OUTPUT AND EXITS 0. That silent success turned a
31
+ * 30s window into a 9.12s clip while every log line reported normal.
32
+ *
33
+ * ── DO NOT ──
34
+ * - Do NOT assume success — callers must handle a null return.
35
+ * - Do NOT put the buffer dir and the output dir on different filesystems.
36
+ * Hardlinks cannot cross a filesystem boundary, so pinning silently degrades
37
+ * to the racy behaviour above (it is logged, but the reels get short again).
38
+ * - Do NOT treat a zero exit from ffmpeg as "the clip is complete" — compare
39
+ * the probed duration against the requested window, which is what the
40
+ * shortfall warning below does.
41
+ */
42
+ export declare class HighlightExtractorService {
43
+ private readonly outputDir;
44
+ private readonly logger;
45
+ private readonly extractTimeoutMs;
46
+ constructor(outputDir: string, logger: Logger);
47
+ /**
48
+ * Concat the overlapping segments and trim to [windowStartMs, windowEndMs].
49
+ * Returns the written clip path, or null on any failure.
50
+ */
51
+ extractWindow(segments: SegmentRecord[], windowStartMs: number, windowEndMs: number, courtId: string): Promise<string | null>;
52
+ /**
53
+ * Hardlink each segment into `pinDir` so the retention sweeper cannot delete
54
+ * the data out from under ffmpeg. Returns the pinned paths, in order.
55
+ *
56
+ * A segment that is ALREADY gone is skipped rather than passed through: the
57
+ * concat demuxer aborts the whole remaining list on a missing file, so
58
+ * skipping one costs a small jump while passing it through costs everything
59
+ * after it.
60
+ */
61
+ private pinSegments;
62
+ /** Actual duration of `file` in seconds, or null if it can't be probed. */
63
+ private probeDurationSec;
64
+ }
65
+ //# sourceMappingURL=HighlightExtractorService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HighlightExtractorService.d.ts","sourceRoot":"","sources":["../../../../src/infrastructure/services/HighlightExtractorService.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAKzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,qBAAa,yBAAyB;IAMlC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJzB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAW;gBAGzB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM;IAGjC;;;OAGG;IACU,aAAa,CACxB,QAAQ,EAAE,aAAa,EAAE,EACzB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA8GzB;;;;;;;;OAQG;IACH,OAAO,CAAC,WAAW;IA8CnB,2EAA2E;IAC3E,OAAO,CAAC,gBAAgB;CAkBzB"}