streamer-node 1.0.31 → 1.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +6 -2
- package/dist/scripts/README.md +670 -0
- package/dist/scripts/install-streamer-node.sh +117 -0
- package/dist/scripts/reframe_ball.py +354 -0
- package/dist/src/application/interfaces/StartStreamUseCase.types.d.ts +3 -1
- package/dist/src/application/interfaces/StartStreamUseCase.types.d.ts.map +1 -1
- package/dist/src/application/interfaces/StartStreamUseCase.types.js.map +1 -1
- package/dist/src/application/services/HttpClient.d.ts +21 -0
- package/dist/src/application/services/HttpClient.d.ts.map +1 -1
- package/dist/src/application/services/HttpClient.js +37 -5
- package/dist/src/application/services/HttpClient.js.map +1 -1
- package/dist/src/application/services/StreamManagerService.d.ts.map +1 -1
- package/dist/src/application/services/StreamManagerService.js +2 -0
- package/dist/src/application/services/StreamManagerService.js.map +1 -1
- package/dist/src/application/use-cases/CaptureHighlightUseCase.d.ts +52 -0
- package/dist/src/application/use-cases/CaptureHighlightUseCase.d.ts.map +1 -0
- package/dist/src/application/use-cases/CaptureHighlightUseCase.js +169 -0
- package/dist/src/application/use-cases/CaptureHighlightUseCase.js.map +1 -0
- package/dist/src/application/use-cases/StartStreamUseCase.d.ts +9 -1
- package/dist/src/application/use-cases/StartStreamUseCase.d.ts.map +1 -1
- package/dist/src/application/use-cases/StartStreamUseCase.js +125 -2
- package/dist/src/application/use-cases/StartStreamUseCase.js.map +1 -1
- package/dist/src/application/use-cases/StopStreamUseCase.d.ts +5 -1
- package/dist/src/application/use-cases/StopStreamUseCase.d.ts.map +1 -1
- package/dist/src/application/use-cases/StopStreamUseCase.js +7 -1
- package/dist/src/application/use-cases/StopStreamUseCase.js.map +1 -1
- package/dist/src/domain/events/StreamEvent.d.ts +5 -0
- package/dist/src/domain/events/StreamEvent.d.ts.map +1 -1
- package/dist/src/domain/services/BallReframer.d.ts +35 -0
- package/dist/src/domain/services/BallReframer.d.ts.map +1 -0
- package/dist/src/domain/services/BallReframer.js +3 -0
- package/dist/src/domain/services/BallReframer.js.map +1 -0
- package/dist/src/domain/services/FFmpegService.d.ts +6 -2
- package/dist/src/domain/services/FFmpegService.d.ts.map +1 -1
- package/dist/src/domain/services/HighlightSignalSource.d.ts +113 -0
- package/dist/src/domain/services/HighlightSignalSource.d.ts.map +1 -0
- package/dist/src/domain/services/HighlightSignalSource.js +3 -0
- package/dist/src/domain/services/HighlightSignalSource.js.map +1 -0
- package/dist/src/domain/services/SSEService.d.ts +1 -0
- package/dist/src/domain/services/SSEService.d.ts.map +1 -1
- package/dist/src/infrastructure/config/Config.d.ts +32 -0
- package/dist/src/infrastructure/config/Config.d.ts.map +1 -1
- package/dist/src/infrastructure/config/Config.js +60 -0
- package/dist/src/infrastructure/config/Config.js.map +1 -1
- package/dist/src/infrastructure/listeners/SupabaseListener.d.ts +5 -0
- package/dist/src/infrastructure/listeners/SupabaseListener.d.ts.map +1 -1
- package/dist/src/infrastructure/listeners/SupabaseListener.js +43 -5
- package/dist/src/infrastructure/listeners/SupabaseListener.js.map +1 -1
- package/dist/src/infrastructure/logging/LogShippingService.d.ts +3 -1
- package/dist/src/infrastructure/logging/LogShippingService.d.ts.map +1 -1
- package/dist/src/infrastructure/logging/LogShippingService.js +13 -4
- package/dist/src/infrastructure/logging/LogShippingService.js.map +1 -1
- package/dist/src/infrastructure/logging/RemoteLogger.d.ts +1 -0
- package/dist/src/infrastructure/logging/RemoteLogger.d.ts.map +1 -1
- package/dist/src/infrastructure/logging/RemoteLogger.js +1 -1
- package/dist/src/infrastructure/logging/RemoteLogger.js.map +1 -1
- package/dist/src/infrastructure/services/AdDownloaderService.d.ts +85 -0
- package/dist/src/infrastructure/services/AdDownloaderService.d.ts.map +1 -0
- package/dist/src/infrastructure/services/AdDownloaderService.js +530 -0
- package/dist/src/infrastructure/services/AdDownloaderService.js.map +1 -0
- package/dist/src/infrastructure/services/AdRotator.d.ts +63 -0
- package/dist/src/infrastructure/services/AdRotator.d.ts.map +1 -0
- package/dist/src/infrastructure/services/AdRotator.js +223 -0
- package/dist/src/infrastructure/services/AdRotator.js.map +1 -0
- package/dist/src/infrastructure/services/HighlightBufferManager.d.ts +65 -0
- package/dist/src/infrastructure/services/HighlightBufferManager.d.ts.map +1 -0
- package/dist/src/infrastructure/services/HighlightBufferManager.js +203 -0
- package/dist/src/infrastructure/services/HighlightBufferManager.js.map +1 -0
- package/dist/src/infrastructure/services/HighlightBufferRegistry.d.ts +23 -0
- package/dist/src/infrastructure/services/HighlightBufferRegistry.d.ts.map +1 -0
- package/dist/src/infrastructure/services/HighlightBufferRegistry.js +44 -0
- package/dist/src/infrastructure/services/HighlightBufferRegistry.js.map +1 -0
- package/dist/src/infrastructure/services/HighlightExtractorService.d.ts +65 -0
- package/dist/src/infrastructure/services/HighlightExtractorService.d.ts.map +1 -0
- package/dist/src/infrastructure/services/HighlightExtractorService.js +264 -0
- package/dist/src/infrastructure/services/HighlightExtractorService.js.map +1 -0
- package/dist/src/infrastructure/services/HighlightRendererService.d.ts +38 -0
- package/dist/src/infrastructure/services/HighlightRendererService.d.ts.map +1 -0
- package/dist/src/infrastructure/services/HighlightRendererService.js +182 -0
- package/dist/src/infrastructure/services/HighlightRendererService.js.map +1 -0
- package/dist/src/infrastructure/services/NodeFFmpegService.d.ts +6 -4
- package/dist/src/infrastructure/services/NodeFFmpegService.d.ts.map +1 -1
- package/dist/src/infrastructure/services/NodeFFmpegService.js +154 -32
- package/dist/src/infrastructure/services/NodeFFmpegService.js.map +1 -1
- package/dist/src/infrastructure/services/NodeSSEService.d.ts +8 -0
- package/dist/src/infrastructure/services/NodeSSEService.d.ts.map +1 -1
- package/dist/src/infrastructure/services/NodeSSEService.js +71 -27
- package/dist/src/infrastructure/services/NodeSSEService.js.map +1 -1
- package/dist/src/infrastructure/services/NullBallReframer.d.ts +11 -0
- package/dist/src/infrastructure/services/NullBallReframer.d.ts.map +1 -0
- package/dist/src/infrastructure/services/NullBallReframer.js +16 -0
- package/dist/src/infrastructure/services/NullBallReframer.js.map +1 -0
- package/dist/src/infrastructure/services/PythonBallReframer.d.ts +35 -0
- package/dist/src/infrastructure/services/PythonBallReframer.d.ts.map +1 -0
- package/dist/src/infrastructure/services/PythonBallReframer.js +238 -0
- package/dist/src/infrastructure/services/PythonBallReframer.js.map +1 -0
- package/dist/src/infrastructure/services/SerialHighlightListener.d.ts +45 -0
- package/dist/src/infrastructure/services/SerialHighlightListener.d.ts.map +1 -0
- package/dist/src/infrastructure/services/SerialHighlightListener.js +440 -0
- package/dist/src/infrastructure/services/SerialHighlightListener.js.map +1 -0
- package/dist/src/infrastructure/services/YouTubeUploadService.d.ts +36 -0
- package/dist/src/infrastructure/services/YouTubeUploadService.d.ts.map +1 -0
- package/dist/src/infrastructure/services/YouTubeUploadService.js +402 -0
- package/dist/src/infrastructure/services/YouTubeUploadService.js.map +1 -0
- package/dist/src/infrastructure/utils/ResourceSampler.d.ts +47 -0
- package/dist/src/infrastructure/utils/ResourceSampler.d.ts.map +1 -0
- package/dist/src/infrastructure/utils/ResourceSampler.js +102 -0
- package/dist/src/infrastructure/utils/ResourceSampler.js.map +1 -0
- package/dist/src/infrastructure/utils/lowPriority.d.ts +29 -0
- package/dist/src/infrastructure/utils/lowPriority.d.ts.map +1 -0
- package/dist/src/infrastructure/utils/lowPriority.js +31 -0
- package/dist/src/infrastructure/utils/lowPriority.js.map +1 -0
- package/dist/src/infrastructure/utils/paths.d.ts +27 -0
- package/dist/src/infrastructure/utils/paths.d.ts.map +1 -0
- package/dist/src/infrastructure/utils/paths.js +69 -0
- package/dist/src/infrastructure/utils/paths.js.map +1 -0
- package/dist/src/infrastructure/utils/spawnToFile.d.ts +20 -0
- package/dist/src/infrastructure/utils/spawnToFile.d.ts.map +1 -0
- package/dist/src/infrastructure/utils/spawnToFile.js +98 -0
- package/dist/src/infrastructure/utils/spawnToFile.js.map +1 -0
- package/dist/src/main.js +228 -2
- package/dist/src/main.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,264 @@
|
|
|
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.HighlightExtractorService = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const paths_1 = require("../utils/paths");
|
|
41
|
+
const spawnToFile_1 = require("../utils/spawnToFile");
|
|
42
|
+
const lowPriority_1 = require("../utils/lowPriority");
|
|
43
|
+
/**
|
|
44
|
+
* Cuts a highlight clip out of the rolling buffer's segment files.
|
|
45
|
+
*
|
|
46
|
+
* ── IN SIMPLE WORDS ──
|
|
47
|
+
* Given the buffered video chunks that overlap a time window, this stitches
|
|
48
|
+
* them together and trims to exactly the window, producing one clip file. It
|
|
49
|
+
* runs a one-shot ffmpeg, completely separate from the live stream.
|
|
50
|
+
*
|
|
51
|
+
* ── BUSINESS RULES ──
|
|
52
|
+
* - The clip spans [windowStart, windowEnd]; the trim offset is measured from
|
|
53
|
+
* the FIRST overlapping segment's start (which sits at or before windowStart,
|
|
54
|
+
* guaranteed by the buffer manifest's leading-edge check).
|
|
55
|
+
* - The extracted clip is RAW (no logos/crop). Ball-tracking reframe + logo
|
|
56
|
+
* overlay (Phase 5) run on this clip afterwards; here we only get the window
|
|
57
|
+
* right.
|
|
58
|
+
*
|
|
59
|
+
* ── WHY IT'S BUILT THIS WAY (change at your peril) ──
|
|
60
|
+
* - Output-side `-ss`/`-t` (after `-i`) + a real re-encode gives a
|
|
61
|
+
* frame-accurate cut. Stream-copy would snap to the nearest keyframe (up to
|
|
62
|
+
* `bufferSegmentSec` off) — unacceptable for a tight highlight window.
|
|
63
|
+
* - temp-file + atomic rename: a killed/failed ffmpeg never leaves a corrupt
|
|
64
|
+
* clip at the final path. Fail-soft: any error returns null (logged), never
|
|
65
|
+
* throws into the caller — a failed highlight must not disrupt anything.
|
|
66
|
+
*
|
|
67
|
+
* - Segments are HARDLINKED into a private dir before ffmpeg reads them. The
|
|
68
|
+
* retention sweeper runs every 5s and this re-encode takes ~30s, so without
|
|
69
|
+
* pinning the sweeper deletes segments ffmpeg hasn't reached yet — and
|
|
70
|
+
* ffmpeg then TRUNCATES THE OUTPUT AND EXITS 0. That silent success turned a
|
|
71
|
+
* 30s window into a 9.12s clip while every log line reported normal.
|
|
72
|
+
*
|
|
73
|
+
* ── DO NOT ──
|
|
74
|
+
* - Do NOT assume success — callers must handle a null return.
|
|
75
|
+
* - Do NOT put the buffer dir and the output dir on different filesystems.
|
|
76
|
+
* Hardlinks cannot cross a filesystem boundary, so pinning silently degrades
|
|
77
|
+
* to the racy behaviour above (it is logged, but the reels get short again).
|
|
78
|
+
* - Do NOT treat a zero exit from ffmpeg as "the clip is complete" — compare
|
|
79
|
+
* the probed duration against the requested window, which is what the
|
|
80
|
+
* shortfall warning below does.
|
|
81
|
+
*/
|
|
82
|
+
class HighlightExtractorService {
|
|
83
|
+
constructor(outputDir, logger) {
|
|
84
|
+
this.outputDir = outputDir;
|
|
85
|
+
this.logger = logger;
|
|
86
|
+
// Generous: runs at low priority (see withLowPriority), so it may take
|
|
87
|
+
// longer under load — better slow than killed. Live stream is protected.
|
|
88
|
+
this.extractTimeoutMs = 120000;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Concat the overlapping segments and trim to [windowStartMs, windowEndMs].
|
|
92
|
+
* Returns the written clip path, or null on any failure.
|
|
93
|
+
*/
|
|
94
|
+
async extractWindow(segments, windowStartMs, windowEndMs, courtId) {
|
|
95
|
+
if (segments.length === 0)
|
|
96
|
+
return null;
|
|
97
|
+
const stamp = windowStartMs; // stable, sortable id for the clip
|
|
98
|
+
const courtOut = path.join(this.outputDir, (0, paths_1.safeSegment)(courtId));
|
|
99
|
+
const listPath = path.join(courtOut, `.concat-${stamp}.txt`);
|
|
100
|
+
const pinDir = path.join(courtOut, `.pin-${stamp}`);
|
|
101
|
+
const dest = path.join(courtOut, `highlight-${stamp}.mp4`);
|
|
102
|
+
try {
|
|
103
|
+
(0, paths_1.ensureDirSync)(courtOut);
|
|
104
|
+
// PIN THE SEGMENTS BEFORE READING THEM.
|
|
105
|
+
//
|
|
106
|
+
// The retention sweeper deletes buffer segments on a 5s timer, and this
|
|
107
|
+
// re-encode takes ~30s at nice 19 on a loaded box. Without pinning, the
|
|
108
|
+
// sweeper unlinks segments this ffmpeg has NOT REACHED YET, ffmpeg hits
|
|
109
|
+
// "Error during demuxing: No such file or directory" — and then EXITS 0
|
|
110
|
+
// with a truncated file. That silent success is what made this so hard
|
|
111
|
+
// to see: a 30s window came out at 9.12s and every log line said fine.
|
|
112
|
+
//
|
|
113
|
+
// A hardlink is a second name for the same inode. Once we hold one, the
|
|
114
|
+
// sweeper's unlink only removes ITS name; the data stays alive until we
|
|
115
|
+
// drop ours in the finally below. No locks, no coordination with the
|
|
116
|
+
// sweeper, and no copy — this is a directory entry, not the video.
|
|
117
|
+
const pinned = this.pinSegments(segments, pinDir, courtId);
|
|
118
|
+
if (pinned.length === 0)
|
|
119
|
+
return null;
|
|
120
|
+
// concat demuxer list. Entries MUST be absolute: ffmpeg resolves relative
|
|
121
|
+
// `file` paths against the LIST FILE's directory (not CWD), so with the
|
|
122
|
+
// default relative bufferDir the segments would be looked up under the
|
|
123
|
+
// output dir and never found. path.resolve makes this unambiguous.
|
|
124
|
+
// Escape single quotes per ffmpeg's list syntax.
|
|
125
|
+
const listBody = pinned
|
|
126
|
+
.map((p) => `file '${path.resolve(p).replace(/'/g, "'\\''")}'`)
|
|
127
|
+
.join("\n");
|
|
128
|
+
fs.writeFileSync(listPath, `${listBody}\n`);
|
|
129
|
+
const offsetSec = Math.max(0, (windowStartMs - segments[0].startMs) / 1000);
|
|
130
|
+
const durationSec = Math.max(0, (windowEndMs - windowStartMs) / 1000);
|
|
131
|
+
if (durationSec <= 0) {
|
|
132
|
+
this.logger.warn("Highlight extraction skipped: non-positive window", {
|
|
133
|
+
courtId,
|
|
134
|
+
windowStartMs,
|
|
135
|
+
windowEndMs,
|
|
136
|
+
});
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
// Low CPU priority so this re-encode can't starve the live ffmpeg.
|
|
140
|
+
const { command, args } = (0, lowPriority_1.withLowPriority)("ffmpeg", [
|
|
141
|
+
"-y",
|
|
142
|
+
"-f", "concat",
|
|
143
|
+
"-safe", "0",
|
|
144
|
+
"-i", listPath,
|
|
145
|
+
// output-side seek/duration → frame-accurate cut of the window
|
|
146
|
+
"-ss", String(offsetSec),
|
|
147
|
+
"-t", String(durationSec),
|
|
148
|
+
"-c:v", "libx264",
|
|
149
|
+
"-preset", "veryfast",
|
|
150
|
+
"-an",
|
|
151
|
+
]);
|
|
152
|
+
await (0, spawnToFile_1.spawnToFile)(command, args, dest, this.extractTimeoutMs);
|
|
153
|
+
// Log the ACTUAL duration alongside the requested one. Logging only the
|
|
154
|
+
// request hid a real bug: the buffer's segments carry the camera
|
|
155
|
+
// sub-stream's timing, so a 30s window can extract to a far shorter clip
|
|
156
|
+
// and nothing in the logs said so. A mismatch here means the reel is
|
|
157
|
+
// time-compressed and every downstream step inherits it.
|
|
158
|
+
const actualSec = await this.probeDurationSec(dest);
|
|
159
|
+
const shortfall = actualSec !== null && durationSec > 0
|
|
160
|
+
? 1 - actualSec / durationSec
|
|
161
|
+
: 0;
|
|
162
|
+
this.logger.info("Highlight clip extracted", {
|
|
163
|
+
courtId,
|
|
164
|
+
dest,
|
|
165
|
+
segments: segments.length,
|
|
166
|
+
offsetSec,
|
|
167
|
+
requestedSec: durationSec,
|
|
168
|
+
actualSec: actualSec !== null ? +actualSec.toFixed(2) : undefined,
|
|
169
|
+
});
|
|
170
|
+
// >10% short is not rounding — it means duration is being lost upstream.
|
|
171
|
+
if (shortfall > 0.1) {
|
|
172
|
+
this.logger.warn("Highlight clip is much shorter than the requested window — the reel will look sped up", {
|
|
173
|
+
courtId,
|
|
174
|
+
requestedSec: durationSec,
|
|
175
|
+
actualSec: actualSec !== null ? +actualSec.toFixed(2) : undefined,
|
|
176
|
+
lostPct: Math.round(shortfall * 100),
|
|
177
|
+
hint: "segments went missing mid-read (ffmpeg truncates and still exits 0) — check the pinning warning above",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return dest;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
this.logger.warn("Highlight extraction failed", {
|
|
184
|
+
courtId,
|
|
185
|
+
error: error instanceof Error ? error.message : String(error),
|
|
186
|
+
});
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
fs.unlink(listPath, () => { }); // best-effort cleanup of the list file
|
|
191
|
+
// Drop our hardlinks. For segments the sweeper already unlinked, this is
|
|
192
|
+
// the last reference and the disk space is reclaimed here.
|
|
193
|
+
fs.rm(pinDir, { recursive: true, force: true }, () => { });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Hardlink each segment into `pinDir` so the retention sweeper cannot delete
|
|
198
|
+
* the data out from under ffmpeg. Returns the pinned paths, in order.
|
|
199
|
+
*
|
|
200
|
+
* A segment that is ALREADY gone is skipped rather than passed through: the
|
|
201
|
+
* concat demuxer aborts the whole remaining list on a missing file, so
|
|
202
|
+
* skipping one costs a small jump while passing it through costs everything
|
|
203
|
+
* after it.
|
|
204
|
+
*/
|
|
205
|
+
pinSegments(segments, pinDir, courtId) {
|
|
206
|
+
(0, paths_1.ensureDirSync)(pinDir);
|
|
207
|
+
const pinned = [];
|
|
208
|
+
let missing = 0;
|
|
209
|
+
let unlinkable = 0;
|
|
210
|
+
for (const seg of segments) {
|
|
211
|
+
const src = path.resolve(seg.path);
|
|
212
|
+
const dst = path.join(pinDir, path.basename(src));
|
|
213
|
+
try {
|
|
214
|
+
fs.linkSync(src, dst);
|
|
215
|
+
pinned.push(dst);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
const code = error.code;
|
|
219
|
+
if (code === "ENOENT") {
|
|
220
|
+
missing++; // swept between the manifest query and now
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// EXDEV (different filesystem) or EPERM: we cannot pin, but the file is
|
|
224
|
+
// there right now. Use it directly and accept the original race rather
|
|
225
|
+
// than dropping footage we can still read.
|
|
226
|
+
unlinkable++;
|
|
227
|
+
pinned.push(src);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (missing > 0 || unlinkable > 0) {
|
|
231
|
+
this.logger.warn("Some highlight segments could not be pinned", {
|
|
232
|
+
courtId,
|
|
233
|
+
missing,
|
|
234
|
+
unlinkable,
|
|
235
|
+
pinned: pinned.length,
|
|
236
|
+
of: segments.length,
|
|
237
|
+
hint: unlinkable > 0
|
|
238
|
+
? "buffer and output dirs are on different filesystems — put them on one volume so segments can be hardlinked"
|
|
239
|
+
: "segments were swept before pinning; raise HIGHLIGHT_BUFFER_RETENTION_SEC",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return pinned;
|
|
243
|
+
}
|
|
244
|
+
/** Actual duration of `file` in seconds, or null if it can't be probed. */
|
|
245
|
+
probeDurationSec(file) {
|
|
246
|
+
return new Promise((resolve) => {
|
|
247
|
+
const proc = (0, child_process_1.spawn)("ffprobe", [
|
|
248
|
+
"-v", "error",
|
|
249
|
+
"-show_entries", "format=duration",
|
|
250
|
+
"-of", "default=nw=1:nk=1",
|
|
251
|
+
file,
|
|
252
|
+
]);
|
|
253
|
+
let out = "";
|
|
254
|
+
proc.stdout?.on("data", (d) => (out += d.toString()));
|
|
255
|
+
proc.on("exit", (code) => {
|
|
256
|
+
const dur = parseFloat(out.trim());
|
|
257
|
+
resolve(code === 0 && Number.isFinite(dur) && dur > 0 ? dur : null);
|
|
258
|
+
});
|
|
259
|
+
proc.on("error", () => resolve(null));
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
exports.HighlightExtractorService = HighlightExtractorService;
|
|
264
|
+
//# sourceMappingURL=HighlightExtractorService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HighlightExtractorService.js","sourceRoot":"","sources":["../../../../src/infrastructure/services/HighlightExtractorService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Logger } from "../../application/interfaces/Logger";
|
|
2
|
+
/**
|
|
3
|
+
* Overlays the DropShot + client logos onto a highlight clip, producing the
|
|
4
|
+
* final reel. Works on either a full-frame (16:9) clip or a reframed (e.g. 9:16)
|
|
5
|
+
* one — logo sizes/positions are derived from the actual canvas dimensions.
|
|
6
|
+
*
|
|
7
|
+
* ── IN SIMPLE WORDS ──
|
|
8
|
+
* Stamps the two logos onto the highlight clip and writes the finished reel.
|
|
9
|
+
* Logos are sized relative to the video so they look right whether the clip is
|
|
10
|
+
* wide or vertical.
|
|
11
|
+
*
|
|
12
|
+
* ── WHY IT'S BUILT THIS WAY (change at your peril) ──
|
|
13
|
+
* - It probes the clip's real dimensions and sizes logos from them, so the same
|
|
14
|
+
* code brands a 1920x1080 full-frame clip and a 608x1080 reel correctly.
|
|
15
|
+
* - Only logos that actually exist on disk are overlaid; a missing client logo
|
|
16
|
+
* yields a clip with just the DropShot mark rather than a hard failure.
|
|
17
|
+
* - temp-file + atomic rename; fail-soft (returns null, logged) so a render
|
|
18
|
+
* problem never loses the underlying clip — the caller keeps the source.
|
|
19
|
+
*
|
|
20
|
+
* ── DO NOT ──
|
|
21
|
+
* - Do NOT assume both logos exist; check each.
|
|
22
|
+
* - Do NOT assume success — callers must handle a null return (fall back to the
|
|
23
|
+
* un-branded source clip).
|
|
24
|
+
*/
|
|
25
|
+
export declare class HighlightRendererService {
|
|
26
|
+
private readonly dsLogoPath;
|
|
27
|
+
private readonly clientLogoPath;
|
|
28
|
+
private readonly logger;
|
|
29
|
+
private readonly renderTimeoutMs;
|
|
30
|
+
constructor(dsLogoPath: string, clientLogoPath: string, logger: Logger);
|
|
31
|
+
/**
|
|
32
|
+
* Overlay logos from `sourceClip` into `outPath`. Returns outPath on success,
|
|
33
|
+
* or null on any failure (caller should fall back to the source clip).
|
|
34
|
+
*/
|
|
35
|
+
render(sourceClip: string, outPath: string, courtId: string): Promise<string | null>;
|
|
36
|
+
private probeDimensions;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=HighlightRendererService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HighlightRendererService.d.ts","sourceRoot":"","sources":["../../../../src/infrastructure/services/HighlightRendererService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAC;AAI7D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,wBAAwB;IAMjC,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAW;gBAGxB,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,MAAM;IAGjC;;;OAGG;IACU,MAAM,CACjB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA8FzB,OAAO,CAAC,eAAe;CAwBxB"}
|
|
@@ -0,0 +1,182 @@
|
|
|
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.HighlightRendererService = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const child_process_1 = require("child_process");
|
|
39
|
+
const spawnToFile_1 = require("../utils/spawnToFile");
|
|
40
|
+
const lowPriority_1 = require("../utils/lowPriority");
|
|
41
|
+
/**
|
|
42
|
+
* Overlays the DropShot + client logos onto a highlight clip, producing the
|
|
43
|
+
* final reel. Works on either a full-frame (16:9) clip or a reframed (e.g. 9:16)
|
|
44
|
+
* one — logo sizes/positions are derived from the actual canvas dimensions.
|
|
45
|
+
*
|
|
46
|
+
* ── IN SIMPLE WORDS ──
|
|
47
|
+
* Stamps the two logos onto the highlight clip and writes the finished reel.
|
|
48
|
+
* Logos are sized relative to the video so they look right whether the clip is
|
|
49
|
+
* wide or vertical.
|
|
50
|
+
*
|
|
51
|
+
* ── WHY IT'S BUILT THIS WAY (change at your peril) ──
|
|
52
|
+
* - It probes the clip's real dimensions and sizes logos from them, so the same
|
|
53
|
+
* code brands a 1920x1080 full-frame clip and a 608x1080 reel correctly.
|
|
54
|
+
* - Only logos that actually exist on disk are overlaid; a missing client logo
|
|
55
|
+
* yields a clip with just the DropShot mark rather than a hard failure.
|
|
56
|
+
* - temp-file + atomic rename; fail-soft (returns null, logged) so a render
|
|
57
|
+
* problem never loses the underlying clip — the caller keeps the source.
|
|
58
|
+
*
|
|
59
|
+
* ── DO NOT ──
|
|
60
|
+
* - Do NOT assume both logos exist; check each.
|
|
61
|
+
* - Do NOT assume success — callers must handle a null return (fall back to the
|
|
62
|
+
* un-branded source clip).
|
|
63
|
+
*/
|
|
64
|
+
class HighlightRendererService {
|
|
65
|
+
constructor(dsLogoPath, clientLogoPath, logger) {
|
|
66
|
+
this.dsLogoPath = dsLogoPath;
|
|
67
|
+
this.clientLogoPath = clientLogoPath;
|
|
68
|
+
this.logger = logger;
|
|
69
|
+
// Generous: runs at low priority (see withLowPriority), so it may take
|
|
70
|
+
// longer under load — better slow than killed. Live stream is protected.
|
|
71
|
+
this.renderTimeoutMs = 120000;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Overlay logos from `sourceClip` into `outPath`. Returns outPath on success,
|
|
75
|
+
* or null on any failure (caller should fall back to the source clip).
|
|
76
|
+
*/
|
|
77
|
+
async render(sourceClip, outPath, courtId) {
|
|
78
|
+
try {
|
|
79
|
+
const dims = await this.probeDimensions(sourceClip);
|
|
80
|
+
if (!dims) {
|
|
81
|
+
this.logger.warn("Highlight render skipped: could not probe clip", {
|
|
82
|
+
courtId,
|
|
83
|
+
sourceClip,
|
|
84
|
+
});
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const { width, height } = dims;
|
|
88
|
+
// Logos present on disk (top-right = client, bottom-right = DropShot),
|
|
89
|
+
// matching the live-stream corners. Skip any that are missing.
|
|
90
|
+
const logos = [];
|
|
91
|
+
if (fs.existsSync(this.dsLogoPath)) {
|
|
92
|
+
logos.push({
|
|
93
|
+
file: this.dsLogoPath,
|
|
94
|
+
pos: `main_w-overlay_w-${Math.round(width * 0.02)}:main_h-overlay_h-${Math.round(height * 0.02)}`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (fs.existsSync(this.clientLogoPath)) {
|
|
98
|
+
logos.push({
|
|
99
|
+
file: this.clientLogoPath,
|
|
100
|
+
pos: `main_w-overlay_w-${Math.round(width * 0.02)}:${Math.round(height * 0.02)}`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (logos.length === 0) {
|
|
104
|
+
this.logger.warn("Highlight render skipped: no logo files present", {
|
|
105
|
+
courtId,
|
|
106
|
+
dsLogoPath: this.dsLogoPath,
|
|
107
|
+
clientLogoPath: this.clientLogoPath,
|
|
108
|
+
});
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
// Logo bounding box relative to canvas so it reads well at any aspect.
|
|
112
|
+
// A portrait (reel) canvas is narrow, so the 16:9 fraction renders the
|
|
113
|
+
// logos too small — give portrait a larger share of the width. Landscape
|
|
114
|
+
// (the full-frame reel) keeps the original sizing, so that output is
|
|
115
|
+
// unchanged.
|
|
116
|
+
const portrait = height > width;
|
|
117
|
+
const boxW = Math.round(width * (portrait ? 0.4 : 0.26));
|
|
118
|
+
const boxH = Math.round(height * (portrait ? 0.18 : 0.13));
|
|
119
|
+
const args = ["-y", "-i", sourceClip];
|
|
120
|
+
logos.forEach((l) => args.push("-i", l.file));
|
|
121
|
+
const steps = [];
|
|
122
|
+
let cur = "0:v";
|
|
123
|
+
logos.forEach((l, i) => {
|
|
124
|
+
const input = i + 1; // logo inputs follow the source at index 0
|
|
125
|
+
steps.push(`[${input}:v] scale=${boxW}:${boxH}:force_original_aspect_ratio=decrease [logo${i}];`);
|
|
126
|
+
const next = i === logos.length - 1 ? "vout" : `v${i}`;
|
|
127
|
+
steps.push(`[${cur}][logo${i}] overlay=${l.pos} [${next}];`);
|
|
128
|
+
cur = next;
|
|
129
|
+
});
|
|
130
|
+
// Drop the trailing ';' on the final step.
|
|
131
|
+
const filter = steps.join(" ").replace(/;\s*$/, "");
|
|
132
|
+
args.push("-filter_complex", filter, "-map", "[vout]", "-c:v", "libx264", "-preset", "veryfast",
|
|
133
|
+
// Web/social-safe output: broadly-compatible pixel format and a moved
|
|
134
|
+
// moov atom so the reel starts playing before it's fully downloaded.
|
|
135
|
+
"-pix_fmt", "yuv420p", "-movflags", "+faststart", "-an");
|
|
136
|
+
// Low CPU priority so logo compositing can't starve the live ffmpeg.
|
|
137
|
+
const lp = (0, lowPriority_1.withLowPriority)("ffmpeg", args);
|
|
138
|
+
await (0, spawnToFile_1.spawnToFile)(lp.command, lp.args, outPath, this.renderTimeoutMs);
|
|
139
|
+
this.logger.info("Highlight reel rendered", {
|
|
140
|
+
courtId,
|
|
141
|
+
outPath,
|
|
142
|
+
width,
|
|
143
|
+
height,
|
|
144
|
+
logos: logos.length,
|
|
145
|
+
});
|
|
146
|
+
return outPath;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
this.logger.warn("Highlight render failed", {
|
|
150
|
+
courtId,
|
|
151
|
+
error: error instanceof Error ? error.message : String(error),
|
|
152
|
+
});
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
probeDimensions(clip) {
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
const proc = (0, child_process_1.spawn)("ffprobe", [
|
|
159
|
+
"-v", "error",
|
|
160
|
+
"-select_streams", "v:0",
|
|
161
|
+
"-show_entries", "stream=width,height",
|
|
162
|
+
"-of", "csv=p=0",
|
|
163
|
+
clip,
|
|
164
|
+
]);
|
|
165
|
+
let out = "";
|
|
166
|
+
proc.stdout?.on("data", (d) => {
|
|
167
|
+
out += d.toString();
|
|
168
|
+
});
|
|
169
|
+
proc.on("exit", (code) => {
|
|
170
|
+
if (code !== 0)
|
|
171
|
+
return resolve(null);
|
|
172
|
+
const m = out.trim().match(/(\d+)\s*,\s*(\d+)/);
|
|
173
|
+
if (!m)
|
|
174
|
+
return resolve(null);
|
|
175
|
+
resolve({ width: parseInt(m[1], 10), height: parseInt(m[2], 10) });
|
|
176
|
+
});
|
|
177
|
+
proc.on("error", () => resolve(null));
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
exports.HighlightRendererService = HighlightRendererService;
|
|
182
|
+
//# sourceMappingURL=HighlightRendererService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HighlightRendererService.js","sourceRoot":"","sources":["../../../../src/infrastructure/services/HighlightRendererService.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FFmpegService, FFmpegCommand, FFmpegProcess } from "../../domain/services/FFmpegService";
|
|
1
|
+
import { FFmpegService, FFmpegCommand, FFmpegProcess, AdOverlayPaths } from "../../domain/services/FFmpegService";
|
|
2
2
|
import { StreamUrl } from "../../domain/value-objects/StreamUrl";
|
|
3
3
|
import { Logger } from "../../application/interfaces/Logger";
|
|
4
4
|
import { StartStreamRequest } from "../../application/interfaces/StartStreamUseCase.types";
|
|
@@ -6,6 +6,7 @@ import { Config } from "../config/Config";
|
|
|
6
6
|
export declare class NodeFFmpegService implements FFmpegService {
|
|
7
7
|
private readonly logger;
|
|
8
8
|
private readonly config;
|
|
9
|
+
private static readonly VIDEO_EXTS;
|
|
9
10
|
private readonly runningProcesses;
|
|
10
11
|
private readonly clientLogoPath;
|
|
11
12
|
private readonly scoreOverlayDir;
|
|
@@ -23,15 +24,16 @@ export declare class NodeFFmpegService implements FFmpegService {
|
|
|
23
24
|
startStream(cameraUrl: StreamUrl, streamKey: string, hasAudio: boolean, courtId: string, retry: {
|
|
24
25
|
event: StartStreamRequest;
|
|
25
26
|
onRetryStream: (event: StartStreamRequest) => Promise<void>;
|
|
26
|
-
}, isScorecardActivated?: boolean): Promise<FFmpegProcess>;
|
|
27
|
+
}, isScorecardActivated?: boolean, adPaths?: AdOverlayPaths, highlightBufferDir?: string | null): Promise<FFmpegProcess>;
|
|
27
28
|
stopStream(pid: number): Promise<void>;
|
|
28
29
|
isProcessRunning(pid: number): Promise<boolean>;
|
|
29
30
|
detectAudio(cameraUrl: StreamUrl): Promise<boolean>;
|
|
30
|
-
buildStreamCommand(cameraUrl: StreamUrl, streamKey: string, hasAudio: boolean, courtId: string, isScorecardActivated?: boolean): FFmpegCommand;
|
|
31
|
+
buildStreamCommand(cameraUrl: StreamUrl, streamKey: string, hasAudio: boolean, courtId: string, isScorecardActivated?: boolean, adPaths?: AdOverlayPaths, highlightBufferDir?: string | null): FFmpegCommand;
|
|
31
32
|
getRunningProcesses(): Promise<FFmpegProcess[]>;
|
|
32
33
|
killAllProcesses(): Promise<void>;
|
|
34
|
+
private buildAdInputFlags;
|
|
33
35
|
private validateImageFiles;
|
|
34
|
-
private
|
|
36
|
+
private resetScoreOverlay;
|
|
35
37
|
private getScoreOverlayPath;
|
|
36
38
|
private createDefaultScoreOverlay;
|
|
37
39
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeFFmpegService.d.ts","sourceRoot":"","sources":["../../../../src/infrastructure/services/NodeFFmpegService.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,aAAa,EACb,aAAa,EACb,aAAa,
|
|
1
|
+
{"version":3,"file":"NodeFFmpegService.d.ts","sourceRoot":"","sources":["../../../../src/infrastructure/services/NodeFFmpegService.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,aAAa,EACb,aAAa,EACb,aAAa,EACb,cAAc,EACf,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uDAAuD,CAAC;AAC3F,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG1C,qBAAa,iBAAkB,YAAW,aAAa;IAQnD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IARzB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAwD;IAE1F,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAyC;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;gBAGtB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM;IAMjC;;;;;;;;;OASG;IACU,WAAW,CACtB,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE;QACL,KAAK,EAAE,kBAAkB,CAAC;QAC1B,aAAa,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC7D,EACD,oBAAoB,CAAC,EAAE,OAAO,EAC9B,OAAO,CAAC,EAAE,cAAc,EACxB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,GACjC,OAAO,CAAC,aAAa,CAAC;IAuIZ,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4DtC,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAU/C,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAkDzD,kBAAkB,CACvB,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,MAAM,EACf,oBAAoB,CAAC,EAAE,OAAO,EAC9B,OAAO,CAAC,EAAE,cAAc,EACxB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,GACjC,aAAa;IAwPH,mBAAmB,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAI/C,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB9C,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,yBAAyB;CAoBlC"}
|