streamer-node 1.0.30 → 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.
- 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,402 @@
|
|
|
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.YouTubeUploadService = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const child_process_1 = require("child_process");
|
|
39
|
+
/**
|
|
40
|
+
* Uploads a finished highlight reel to YouTube via the DropShot backend's
|
|
41
|
+
* resumable upload-session flow.
|
|
42
|
+
*
|
|
43
|
+
* ── IN SIMPLE WORDS ──
|
|
44
|
+
* After a reel is built on disk, this: (1) asks our backend for a one-time
|
|
45
|
+
* Google upload URL, (2) sends the video file straight to Google in chunks
|
|
46
|
+
* (retrying/resuming if a chunk drops), then (3) tells our backend the result
|
|
47
|
+
* (the YouTube video id, or that it failed). Our backend never sees the video
|
|
48
|
+
* bytes — only Google does.
|
|
49
|
+
*
|
|
50
|
+
* ── BUSINESS RULES ──
|
|
51
|
+
* - The upload-session request needs the court's `streamKey`: the backend reads
|
|
52
|
+
* the video's PRIVACY off the stream_records row matching that key, scoped to
|
|
53
|
+
* the court. A wrong/absent key → the backend 404s (never a public default).
|
|
54
|
+
* - `isShort` is only claimed when the reel's real dimensions (probed here with
|
|
55
|
+
* ffprobe) resolve to 9:16 or 1:1 — the only ratios the backend accepts for a
|
|
56
|
+
* Short. Any other ratio (e.g. our 4:5 default) is uploaded as a regular
|
|
57
|
+
* video, which has no ratio constraint. This avoids a guaranteed rejection.
|
|
58
|
+
* - Chunks are 5 MB (a multiple of 262144, as Google's resumable protocol
|
|
59
|
+
* requires for every chunk except the last). On a 308 we trust Google's
|
|
60
|
+
* `Range` header for how much it actually kept, over our own counter.
|
|
61
|
+
*
|
|
62
|
+
* ── WHY IT'S BUILT THIS WAY (change at your peril) ──
|
|
63
|
+
* - FAIL-SOFT: any failure logs and returns null — a failed upload must never
|
|
64
|
+
* throw into the capture pipeline or affect the live stream, and the reel is
|
|
65
|
+
* already safely on local disk regardless.
|
|
66
|
+
* - The `sessionUri` is a SECRET (it's a pre-authorized write URL to Google).
|
|
67
|
+
* It is never logged and never shipped to the logs endpoint — only used to
|
|
68
|
+
* PUT bytes. Do not add it to any logger call.
|
|
69
|
+
*
|
|
70
|
+
* ── DO NOT ──
|
|
71
|
+
* - Do NOT send the x-streaming-api-key to Google (step 6 PUTs go direct to the
|
|
72
|
+
* sessionUri with NO backend auth). Only steps 5 and 7 hit our backend.
|
|
73
|
+
* - Do NOT log sessionUri, tokens, or the stream key.
|
|
74
|
+
*/
|
|
75
|
+
const CHUNK_SIZE = 5 * 1024 * 1024; // 5 MB — multiple of 262144, Google-legal
|
|
76
|
+
const SESSION_TIMEOUT_MS = 30000;
|
|
77
|
+
const CHUNK_TIMEOUT_MS = 120000;
|
|
78
|
+
const MAX_CHUNK_RETRIES = 5;
|
|
79
|
+
class YouTubeUploadService {
|
|
80
|
+
constructor(baseUrl, groundId, streamingApiKey, logger) {
|
|
81
|
+
this.baseUrl = baseUrl;
|
|
82
|
+
this.groundId = groundId;
|
|
83
|
+
this.streamingApiKey = streamingApiKey;
|
|
84
|
+
this.logger = logger;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Upload `filePath` for `courtId`. Returns the YouTube video id on success,
|
|
88
|
+
* or null on any failure (logged). Reports the outcome to the backend either
|
|
89
|
+
* way (so a device-reported failure is recorded).
|
|
90
|
+
*/
|
|
91
|
+
async upload(req) {
|
|
92
|
+
if (!this.streamingApiKey) {
|
|
93
|
+
this.logger.warn("Reel upload skipped: STREAMING_API_KEY not set", {
|
|
94
|
+
courtId: req.courtId,
|
|
95
|
+
});
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (!fs.existsSync(req.filePath)) {
|
|
99
|
+
this.logger.warn("Reel upload skipped: file missing", {
|
|
100
|
+
courtId: req.courtId,
|
|
101
|
+
filePath: req.filePath,
|
|
102
|
+
});
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const fileSize = fs.statSync(req.filePath).size;
|
|
106
|
+
const dims = await this.probeDimensions(req.filePath);
|
|
107
|
+
const durationSeconds = await this.probeDurationSec(req.filePath);
|
|
108
|
+
// Only claim isShort when the reel truly is Short-legal (9:16 or 1:1) AND
|
|
109
|
+
// within the 60s cap; otherwise upload as a normal video.
|
|
110
|
+
const shortLegal = !!dims &&
|
|
111
|
+
!!durationSeconds &&
|
|
112
|
+
durationSeconds >= 1 &&
|
|
113
|
+
durationSeconds <= 60 &&
|
|
114
|
+
(this.isRatio(dims, 9, 16) || this.isRatio(dims, 1, 1));
|
|
115
|
+
const isShort = (req.preferShort ?? false) && shortLegal;
|
|
116
|
+
if ((req.preferShort ?? false) && !shortLegal) {
|
|
117
|
+
this.logger.info("Reel not Short-legal (need 9:16 or 1:1, ≤60s); uploading as regular video", { courtId: req.courtId, dims, durationSeconds });
|
|
118
|
+
}
|
|
119
|
+
// Logged BEFORE the session request: that call reaches the backend and can
|
|
120
|
+
// hang or be slow, and without this line the whole upload is invisible
|
|
121
|
+
// until it succeeds. "Started but never finished" must be diagnosable.
|
|
122
|
+
const uploadStartedAt = Date.now();
|
|
123
|
+
this.logger.info("Reel upload starting", {
|
|
124
|
+
courtId: req.courtId,
|
|
125
|
+
filePath: req.filePath,
|
|
126
|
+
fileSizeMb: +(fileSize / 1048576).toFixed(2),
|
|
127
|
+
isShort,
|
|
128
|
+
durationSeconds: durationSeconds ? Math.round(durationSeconds) : undefined,
|
|
129
|
+
dims: dims ? `${dims.width}x${dims.height}` : undefined,
|
|
130
|
+
});
|
|
131
|
+
let uploadId;
|
|
132
|
+
try {
|
|
133
|
+
const session = await this.createSession(req, fileSize, isShort, dims, durationSeconds);
|
|
134
|
+
uploadId = session.uploadId;
|
|
135
|
+
this.logger.info("Reel upload session created", {
|
|
136
|
+
courtId: req.courtId,
|
|
137
|
+
uploadId,
|
|
138
|
+
isShort,
|
|
139
|
+
fileSize,
|
|
140
|
+
});
|
|
141
|
+
const videoId = await this.putFile(req.filePath, fileSize, session.sessionUri, req.courtId, uploadId);
|
|
142
|
+
if (!videoId) {
|
|
143
|
+
await this.reportComplete(req.courtId, uploadId, {
|
|
144
|
+
status: "failed",
|
|
145
|
+
error: "resumable upload did not return a video id",
|
|
146
|
+
});
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
await this.reportComplete(req.courtId, uploadId, { videoId });
|
|
150
|
+
this.logger.info("Reel uploaded", {
|
|
151
|
+
courtId: req.courtId,
|
|
152
|
+
uploadId,
|
|
153
|
+
videoId,
|
|
154
|
+
url: `https://youtu.be/${videoId}`,
|
|
155
|
+
...this.transferStats(fileSize, uploadStartedAt),
|
|
156
|
+
});
|
|
157
|
+
return videoId;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
161
|
+
this.logger.warn("Reel upload failed", {
|
|
162
|
+
courtId: req.courtId,
|
|
163
|
+
uploadId,
|
|
164
|
+
error: msg,
|
|
165
|
+
elapsedSec: +((Date.now() - uploadStartedAt) / 1000).toFixed(1),
|
|
166
|
+
});
|
|
167
|
+
// Best-effort failure report so the backend records it (idempotent).
|
|
168
|
+
if (uploadId) {
|
|
169
|
+
await this.reportComplete(req.courtId, uploadId, {
|
|
170
|
+
status: "failed",
|
|
171
|
+
error: msg.slice(0, 300),
|
|
172
|
+
}).catch(() => { });
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Step 5 — authorize the resumable upload; returns { uploadId, sessionUri }.
|
|
178
|
+
async createSession(req, fileSize, isShort, dims, durationSeconds) {
|
|
179
|
+
const url = `${this.baseUrl}/api/v1/padel-grounds/${this.groundId}/courts/${req.courtId}/videos/upload-session`;
|
|
180
|
+
const body = {
|
|
181
|
+
title: req.title.slice(0, 90),
|
|
182
|
+
streamKey: req.streamKey,
|
|
183
|
+
fileSize,
|
|
184
|
+
fileType: "video/mp4",
|
|
185
|
+
};
|
|
186
|
+
if (req.description)
|
|
187
|
+
body.description = req.description.slice(0, 5000);
|
|
188
|
+
if (isShort && dims && durationSeconds) {
|
|
189
|
+
body.isShort = true;
|
|
190
|
+
body.durationSeconds = Math.round(durationSeconds);
|
|
191
|
+
body.width = dims.width;
|
|
192
|
+
body.height = dims.height;
|
|
193
|
+
}
|
|
194
|
+
const res = await this.fetchWithTimeout(url, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
"x-streaming-api-key": this.streamingApiKey,
|
|
199
|
+
},
|
|
200
|
+
body: JSON.stringify(body),
|
|
201
|
+
}, SESSION_TIMEOUT_MS);
|
|
202
|
+
if (!res.ok) {
|
|
203
|
+
throw new Error(`upload-session ${res.status}: ${(await this.safeText(res)).slice(0, 200)}`);
|
|
204
|
+
}
|
|
205
|
+
const json = (await res.json());
|
|
206
|
+
if (!json.uploadId || !json.sessionUri) {
|
|
207
|
+
throw new Error("upload-session response missing uploadId/sessionUri");
|
|
208
|
+
}
|
|
209
|
+
return { uploadId: json.uploadId, sessionUri: json.sessionUri };
|
|
210
|
+
}
|
|
211
|
+
// Step 6 — PUT the file to Google in chunks, resuming per the Range header.
|
|
212
|
+
// Returns the video id from the final 200/201, or null.
|
|
213
|
+
async putFile(filePath, fileSize, sessionUri, courtId, uploadId) {
|
|
214
|
+
const totalChunks = Math.max(1, Math.ceil(fileSize / CHUNK_SIZE));
|
|
215
|
+
const startedAt = Date.now();
|
|
216
|
+
let chunkNo = 0;
|
|
217
|
+
let offset = 0;
|
|
218
|
+
while (offset < fileSize) {
|
|
219
|
+
const end = Math.min(offset + CHUNK_SIZE, fileSize);
|
|
220
|
+
const chunk = this.readChunk(filePath, offset, end - offset);
|
|
221
|
+
const contentRange = `bytes ${offset}-${end - 1}/${fileSize}`;
|
|
222
|
+
chunkNo++;
|
|
223
|
+
let attempt = 0;
|
|
224
|
+
// Retry a single chunk on transient failure; on a 308 advance to where
|
|
225
|
+
// Google says it actually is (authoritative over our own offset).
|
|
226
|
+
// eslint-disable-next-line no-constant-condition
|
|
227
|
+
while (true) {
|
|
228
|
+
const res = await this.fetchWithTimeout(sessionUri, {
|
|
229
|
+
method: "PUT",
|
|
230
|
+
headers: {
|
|
231
|
+
"Content-Type": "video/mp4",
|
|
232
|
+
"Content-Range": contentRange,
|
|
233
|
+
},
|
|
234
|
+
body: chunk,
|
|
235
|
+
}, CHUNK_TIMEOUT_MS);
|
|
236
|
+
if (res.status === 200 || res.status === 201) {
|
|
237
|
+
const json = (await res.json().catch(() => ({})));
|
|
238
|
+
this.logger.info("Reel upload transfer complete", {
|
|
239
|
+
courtId,
|
|
240
|
+
uploadId,
|
|
241
|
+
chunks: chunkNo,
|
|
242
|
+
...this.transferStats(fileSize, startedAt),
|
|
243
|
+
});
|
|
244
|
+
return json.id ?? null;
|
|
245
|
+
}
|
|
246
|
+
if (res.status === 308) {
|
|
247
|
+
const kept = this.parseRangeEnd(res.headers.get("range"));
|
|
248
|
+
offset = kept !== null ? kept + 1 : end; // trust Google's Range
|
|
249
|
+
// Per-chunk progress: without this the whole transfer is silent
|
|
250
|
+
// between "session created" and "uploaded", so a stall is
|
|
251
|
+
// indistinguishable from normal operation.
|
|
252
|
+
this.logger.debug("Reel upload progress", {
|
|
253
|
+
courtId,
|
|
254
|
+
uploadId,
|
|
255
|
+
chunk: `${chunkNo}/${totalChunks}`,
|
|
256
|
+
sentMb: +(offset / 1048576).toFixed(2),
|
|
257
|
+
totalMb: +(fileSize / 1048576).toFixed(2),
|
|
258
|
+
percent: Math.round((offset / fileSize) * 100),
|
|
259
|
+
});
|
|
260
|
+
break; // proceed to next chunk
|
|
261
|
+
}
|
|
262
|
+
// Non-terminal error → retry this chunk a few times, else give up.
|
|
263
|
+
if (attempt >= MAX_CHUNK_RETRIES) {
|
|
264
|
+
throw new Error(`chunk PUT failed at ${contentRange}: status ${res.status}`);
|
|
265
|
+
}
|
|
266
|
+
attempt++;
|
|
267
|
+
// Surface retries: a flaky link should be visible, not silent.
|
|
268
|
+
this.logger.warn("Reel upload chunk retry", {
|
|
269
|
+
courtId,
|
|
270
|
+
uploadId,
|
|
271
|
+
chunk: `${chunkNo}/${totalChunks}`,
|
|
272
|
+
status: res.status,
|
|
273
|
+
attempt,
|
|
274
|
+
maxAttempts: MAX_CHUNK_RETRIES,
|
|
275
|
+
});
|
|
276
|
+
await this.delay(1000 * Math.pow(2, attempt - 1));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Loop exited without a 200/201 (e.g. last 308 reported full size) — the
|
|
280
|
+
// caller treats a null video id as a failure and reports it.
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
// Step 7 — report the outcome to the backend (idempotent, safe to repeat).
|
|
284
|
+
async reportComplete(courtId, uploadId, payload) {
|
|
285
|
+
const url = `${this.baseUrl}/api/v1/padel-grounds/${this.groundId}/courts/${courtId}/videos/upload-session/${uploadId}/complete`;
|
|
286
|
+
const res = await this.fetchWithTimeout(url, {
|
|
287
|
+
method: "POST",
|
|
288
|
+
headers: {
|
|
289
|
+
"Content-Type": "application/json",
|
|
290
|
+
"x-streaming-api-key": this.streamingApiKey,
|
|
291
|
+
},
|
|
292
|
+
body: JSON.stringify(payload),
|
|
293
|
+
}, SESSION_TIMEOUT_MS);
|
|
294
|
+
if (!res.ok) {
|
|
295
|
+
// Log but don't throw: the reel is done and (on success) already on
|
|
296
|
+
// YouTube; a failed completion report is a backend-side reconciliation
|
|
297
|
+
// concern, not a device failure.
|
|
298
|
+
this.logger.warn("Reel upload completion report failed", {
|
|
299
|
+
courtId,
|
|
300
|
+
uploadId,
|
|
301
|
+
status: res.status,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// ── helpers ──
|
|
306
|
+
// Returns a plain ArrayBuffer — an unambiguous fetch BodyInit across the DOM
|
|
307
|
+
// and undici typings (a Node Buffer/Uint8Array trips a TS generic mismatch,
|
|
308
|
+
// though it works at runtime).
|
|
309
|
+
readChunk(filePath, start, length) {
|
|
310
|
+
const fd = fs.openSync(filePath, "r");
|
|
311
|
+
try {
|
|
312
|
+
const buf = Buffer.alloc(length);
|
|
313
|
+
fs.readSync(fd, buf, 0, length, start);
|
|
314
|
+
// Copy out an exact-length slice so no extra pool bytes leak into the body.
|
|
315
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
fs.closeSync(fd);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Google's 308 Range header looks like "bytes=0-524287"; return the end index.
|
|
322
|
+
parseRangeEnd(range) {
|
|
323
|
+
if (!range)
|
|
324
|
+
return null;
|
|
325
|
+
const m = range.match(/bytes=\d+-(\d+)/);
|
|
326
|
+
return m ? parseInt(m[1], 10) : null;
|
|
327
|
+
}
|
|
328
|
+
isRatio(dims, w, h) {
|
|
329
|
+
// Allow ~1% tolerance for rounding (e.g. 1080x1920).
|
|
330
|
+
const target = w / h;
|
|
331
|
+
const actual = dims.width / dims.height;
|
|
332
|
+
return Math.abs(actual - target) / target <= 0.01;
|
|
333
|
+
}
|
|
334
|
+
probeDimensions(file) {
|
|
335
|
+
return new Promise((resolve) => {
|
|
336
|
+
const proc = (0, child_process_1.spawn)("ffprobe", [
|
|
337
|
+
"-v", "error",
|
|
338
|
+
"-select_streams", "v:0",
|
|
339
|
+
"-show_entries", "stream=width,height",
|
|
340
|
+
"-of", "csv=p=0",
|
|
341
|
+
file,
|
|
342
|
+
]);
|
|
343
|
+
let out = "";
|
|
344
|
+
proc.stdout?.on("data", (d) => (out += d.toString()));
|
|
345
|
+
proc.on("exit", (code) => {
|
|
346
|
+
if (code !== 0)
|
|
347
|
+
return resolve(null);
|
|
348
|
+
const m = out.trim().match(/(\d+)\s*,\s*(\d+)/);
|
|
349
|
+
resolve(m ? { width: parseInt(m[1], 10), height: parseInt(m[2], 10) } : null);
|
|
350
|
+
});
|
|
351
|
+
proc.on("error", () => resolve(null));
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
probeDurationSec(file) {
|
|
355
|
+
return new Promise((resolve) => {
|
|
356
|
+
const proc = (0, child_process_1.spawn)("ffprobe", [
|
|
357
|
+
"-v", "error",
|
|
358
|
+
"-show_entries", "format=duration",
|
|
359
|
+
"-of", "default=nw=1:nk=1",
|
|
360
|
+
file,
|
|
361
|
+
]);
|
|
362
|
+
let out = "";
|
|
363
|
+
proc.stdout?.on("data", (d) => (out += d.toString()));
|
|
364
|
+
proc.on("exit", (code) => {
|
|
365
|
+
const dur = parseFloat(out.trim());
|
|
366
|
+
resolve(code === 0 && Number.isFinite(dur) && dur > 0 ? dur : null);
|
|
367
|
+
});
|
|
368
|
+
proc.on("error", () => resolve(null));
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async fetchWithTimeout(url, init, timeoutMs) {
|
|
372
|
+
const controller = new AbortController();
|
|
373
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
374
|
+
try {
|
|
375
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
clearTimeout(timer);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async safeText(res) {
|
|
382
|
+
try {
|
|
383
|
+
return await res.text();
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return "";
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/** Elapsed time + average throughput, so a slow link is visible in the logs. */
|
|
390
|
+
transferStats(bytes, startedAt) {
|
|
391
|
+
const elapsedSec = Math.max(0.001, (Date.now() - startedAt) / 1000);
|
|
392
|
+
return {
|
|
393
|
+
elapsedSec: +elapsedSec.toFixed(1),
|
|
394
|
+
mbps: +((bytes * 8) / 1e6 / elapsedSec).toFixed(2),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
delay(ms) {
|
|
398
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
exports.YouTubeUploadService = YouTubeUploadService;
|
|
402
|
+
//# sourceMappingURL=YouTubeUploadService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"YouTubeUploadService.js","sourceRoot":"","sources":["../../../../src/infrastructure/services/YouTubeUploadService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Samples whole-machine CPU + memory (and this process's RSS) at an interval,
|
|
3
|
+
* so a caller can report the resource spike a piece of work caused.
|
|
4
|
+
*
|
|
5
|
+
* ── IN SIMPLE WORDS ──
|
|
6
|
+
* Start it, do some heavy work, stop it — it tells you how busy the box's CPU
|
|
7
|
+
* got, how much RAM was in use before vs at the peak, and the machine load.
|
|
8
|
+
* Used to record, in the logs, exactly what building a highlight reel costs
|
|
9
|
+
* the streamer box (extract + reframe + render are CPU-heavy).
|
|
10
|
+
*
|
|
11
|
+
* ── WHY IT'S BUILT THIS WAY (change at your peril) ──
|
|
12
|
+
* - CPU% is derived from `os.cpus()` cumulative times (deltas between ticks),
|
|
13
|
+
* the same source as /proc/stat — a whole-machine 0-100 figure independent of
|
|
14
|
+
* core count. Peak matters more than average for spotting contention.
|
|
15
|
+
* - The interval timer is `unref()`d so it can NEVER keep the process alive; if
|
|
16
|
+
* stop() is somehow missed, the timer won't hold the event loop open.
|
|
17
|
+
* - Zero dependencies and pure Node os/process — safe to run on every box.
|
|
18
|
+
*
|
|
19
|
+
* ── DO NOT ──
|
|
20
|
+
* - Do NOT treat the numbers as per-process attribution: CPU% is the whole box
|
|
21
|
+
* (which is the point — it captures the live encoder + the reel subprocesses
|
|
22
|
+
* together). The baseline→peak memory delta is what the reel work added.
|
|
23
|
+
*/
|
|
24
|
+
export interface ResourceSummary {
|
|
25
|
+
cores: number;
|
|
26
|
+
samples: number;
|
|
27
|
+
cpuBusyPctPeak: number;
|
|
28
|
+
cpuBusyPctAvg: number;
|
|
29
|
+
memUsedMbBaseline: number;
|
|
30
|
+
memUsedMbPeak: number;
|
|
31
|
+
memTotalMb: number;
|
|
32
|
+
nodeRssMbPeak: number;
|
|
33
|
+
load1: number;
|
|
34
|
+
}
|
|
35
|
+
export declare class ResourceSampler {
|
|
36
|
+
private timer;
|
|
37
|
+
private prev;
|
|
38
|
+
private readonly cpu;
|
|
39
|
+
private readonly memUsed;
|
|
40
|
+
private readonly rss;
|
|
41
|
+
start(intervalMs?: number): void;
|
|
42
|
+
stop(): ResourceSummary;
|
|
43
|
+
private cpuTimes;
|
|
44
|
+
private usedMb;
|
|
45
|
+
private tick;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=ResourceSampler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceSampler.d.ts","sourceRoot":"","sources":["../../../../src/infrastructure/utils/ResourceSampler.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,IAAI,CAAgD;IAC5D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAgB;IAE7B,KAAK,CAAC,UAAU,SAAO,GAAG,IAAI;IAQ9B,IAAI,IAAI,eAAe;IAqB9B,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,IAAI;CAcb"}
|
|
@@ -0,0 +1,102 @@
|
|
|
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.ResourceSampler = void 0;
|
|
37
|
+
const os = __importStar(require("os"));
|
|
38
|
+
class ResourceSampler {
|
|
39
|
+
constructor() {
|
|
40
|
+
this.timer = null;
|
|
41
|
+
this.prev = null;
|
|
42
|
+
this.cpu = [];
|
|
43
|
+
this.memUsed = [];
|
|
44
|
+
this.rss = [];
|
|
45
|
+
}
|
|
46
|
+
start(intervalMs = 1000) {
|
|
47
|
+
if (this.timer)
|
|
48
|
+
return;
|
|
49
|
+
this.prev = this.cpuTimes();
|
|
50
|
+
this.timer = setInterval(() => this.tick(), intervalMs);
|
|
51
|
+
// Never let sampling hold the process open.
|
|
52
|
+
this.timer.unref?.();
|
|
53
|
+
}
|
|
54
|
+
stop() {
|
|
55
|
+
if (this.timer) {
|
|
56
|
+
clearInterval(this.timer);
|
|
57
|
+
this.timer = null;
|
|
58
|
+
}
|
|
59
|
+
const peak = (a) => (a.length ? Math.max(...a) : 0);
|
|
60
|
+
const avg = (a) => a.length ? Math.round(a.reduce((s, x) => s + x, 0) / a.length) : 0;
|
|
61
|
+
return {
|
|
62
|
+
cores: os.cpus().length,
|
|
63
|
+
samples: this.cpu.length,
|
|
64
|
+
cpuBusyPctPeak: peak(this.cpu),
|
|
65
|
+
cpuBusyPctAvg: avg(this.cpu),
|
|
66
|
+
memUsedMbBaseline: this.memUsed[0] ?? this.usedMb(),
|
|
67
|
+
memUsedMbPeak: peak(this.memUsed),
|
|
68
|
+
memTotalMb: Math.round(os.totalmem() / 1048576),
|
|
69
|
+
nodeRssMbPeak: peak(this.rss),
|
|
70
|
+
load1: Math.round((os.loadavg()[0] ?? 0) * 100) / 100,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
cpuTimes() {
|
|
74
|
+
let idle = 0;
|
|
75
|
+
let total = 0;
|
|
76
|
+
for (const c of os.cpus()) {
|
|
77
|
+
const t = c.times;
|
|
78
|
+
idle += t.idle;
|
|
79
|
+
total += t.user + t.nice + t.sys + t.idle + t.irq;
|
|
80
|
+
}
|
|
81
|
+
return { idle, total };
|
|
82
|
+
}
|
|
83
|
+
usedMb() {
|
|
84
|
+
return Math.round((os.totalmem() - os.freemem()) / 1048576);
|
|
85
|
+
}
|
|
86
|
+
tick() {
|
|
87
|
+
const cur = this.cpuTimes();
|
|
88
|
+
if (this.prev) {
|
|
89
|
+
const didle = cur.idle - this.prev.idle;
|
|
90
|
+
const dtot = cur.total - this.prev.total;
|
|
91
|
+
if (dtot > 0) {
|
|
92
|
+
const busy = Math.round(100 * (1 - didle / dtot));
|
|
93
|
+
this.cpu.push(Math.max(0, Math.min(100, busy)));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.prev = cur;
|
|
97
|
+
this.memUsed.push(this.usedMb());
|
|
98
|
+
this.rss.push(Math.round(process.memoryUsage().rss / 1048576));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.ResourceSampler = ResourceSampler;
|
|
102
|
+
//# sourceMappingURL=ResourceSampler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceSampler.js","sourceRoot":"","sources":["../../../../src/infrastructure/utils/ResourceSampler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap a command so a heavy one-shot HIGHLIGHT job runs at low CPU priority.
|
|
3
|
+
*
|
|
4
|
+
* ── IN SIMPLE WORDS ──
|
|
5
|
+
* The reel jobs (extract / reframe / render) are CPU-hungry. On a weak box they
|
|
6
|
+
* can hog the processor and slow down the LIVE stream's ffmpeg enough that its
|
|
7
|
+
* progress clock stalls — and the stall detector then restarts the live stream.
|
|
8
|
+
* Running these jobs with `nice` tells the OS "let the live stream go first",
|
|
9
|
+
* so a reel job can never starve the broadcast.
|
|
10
|
+
*
|
|
11
|
+
* ── WHY IT'S BUILT THIS WAY (change at your peril) ──
|
|
12
|
+
* - `nice -n 19` (lowest priority) is from coreutils — present on every Linux
|
|
13
|
+
* box and macOS, so this needs nothing installed. If `nice` were somehow
|
|
14
|
+
* missing the spawn just errors, and every highlight path is already
|
|
15
|
+
* fail-soft (returns null → full-frame fallback), so the live stream is never
|
|
16
|
+
* affected either way.
|
|
17
|
+
* - Deliberately NOT using `ionice`: it lives in util-linux and could be absent,
|
|
18
|
+
* which would turn a perf tweak into a hard failure. The contention that
|
|
19
|
+
* trips the stall detector is CPU, not disk, so `nice` is the right lever.
|
|
20
|
+
*
|
|
21
|
+
* ── DO NOT ──
|
|
22
|
+
* - Do NOT use this to wrap the LIVE stream ffmpeg — it must keep normal
|
|
23
|
+
* priority; that's the whole point.
|
|
24
|
+
*/
|
|
25
|
+
export declare function withLowPriority(command: string, args: string[]): {
|
|
26
|
+
command: string;
|
|
27
|
+
args: string[];
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=lowPriority.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lowPriority.d.ts","sourceRoot":"","sources":["../../../../src/infrastructure/utils/lowPriority.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,GACb;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAErC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withLowPriority = withLowPriority;
|
|
4
|
+
/**
|
|
5
|
+
* Wrap a command so a heavy one-shot HIGHLIGHT job runs at low CPU priority.
|
|
6
|
+
*
|
|
7
|
+
* ── IN SIMPLE WORDS ──
|
|
8
|
+
* The reel jobs (extract / reframe / render) are CPU-hungry. On a weak box they
|
|
9
|
+
* can hog the processor and slow down the LIVE stream's ffmpeg enough that its
|
|
10
|
+
* progress clock stalls — and the stall detector then restarts the live stream.
|
|
11
|
+
* Running these jobs with `nice` tells the OS "let the live stream go first",
|
|
12
|
+
* so a reel job can never starve the broadcast.
|
|
13
|
+
*
|
|
14
|
+
* ── WHY IT'S BUILT THIS WAY (change at your peril) ──
|
|
15
|
+
* - `nice -n 19` (lowest priority) is from coreutils — present on every Linux
|
|
16
|
+
* box and macOS, so this needs nothing installed. If `nice` were somehow
|
|
17
|
+
* missing the spawn just errors, and every highlight path is already
|
|
18
|
+
* fail-soft (returns null → full-frame fallback), so the live stream is never
|
|
19
|
+
* affected either way.
|
|
20
|
+
* - Deliberately NOT using `ionice`: it lives in util-linux and could be absent,
|
|
21
|
+
* which would turn a perf tweak into a hard failure. The contention that
|
|
22
|
+
* trips the stall detector is CPU, not disk, so `nice` is the right lever.
|
|
23
|
+
*
|
|
24
|
+
* ── DO NOT ──
|
|
25
|
+
* - Do NOT use this to wrap the LIVE stream ffmpeg — it must keep normal
|
|
26
|
+
* priority; that's the whole point.
|
|
27
|
+
*/
|
|
28
|
+
function withLowPriority(command, args) {
|
|
29
|
+
return { command: "nice", args: ["-n", "19", command, ...args] };
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=lowPriority.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lowPriority.js","sourceRoot":"","sources":["../../../../src/infrastructure/utils/lowPriority.ts"],"names":[],"mappings":";;AAwBA,0CAKC;AA7BD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,SAAgB,eAAe,CAC7B,OAAe,EACf,IAAc;IAEd,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;AACnE,CAAC"}
|