@lightcone-ai/daemon 0.23.6 → 0.23.8

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.
@@ -1,6 +1,6 @@
1
1
  // V6 record_url_narration daemon tool wrapper.
2
2
  //
3
- // Drives Chromium on Xvfb + Playwright recordVideo to capture a silent mp4
3
+ // Drives headless Chromium + Playwright recordVideo to capture a silent mp4
4
4
  // per section, then ffmpeg-transcodes + slices. The resulting silent mp4s
5
5
  // feed compose_video_v2 as video-kind segments alongside narration audio.
6
6
  //
@@ -1,126 +0,0 @@
1
- const DEFAULT_BASE_DISPLAY = 99;
2
- const DEFAULT_POOL_SIZE = 3;
3
-
4
- function normalizePositiveInteger(value, fallback) {
5
- const parsed = Number.parseInt(String(value ?? ''), 10);
6
- if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
7
- return parsed;
8
- }
9
-
10
- function createAcquireTimeoutError(timeoutMs) {
11
- const error = new Error(`display_pool_acquire_timeout:${timeoutMs}ms`);
12
- error.code = 'DISPLAY_POOL_ACQUIRE_TIMEOUT';
13
- return error;
14
- }
15
-
16
- function createInvalidPoolError() {
17
- const error = new Error('display_pool_invalid_configuration');
18
- error.code = 'DISPLAY_POOL_INVALID_CONFIGURATION';
19
- return error;
20
- }
21
-
22
- function createSlots({ baseDisplay, poolSize }) {
23
- return Array.from({ length: poolSize }, (_unused, index) => {
24
- const displayNumber = baseDisplay + index;
25
- return {
26
- displayNumber,
27
- display: `:${displayNumber}`,
28
- inUse: false,
29
- };
30
- });
31
- }
32
-
33
- function createLease(slot, releaseSlot) {
34
- let released = false;
35
- return Object.freeze({
36
- display: slot.display,
37
- displayNumber: slot.displayNumber,
38
- release() {
39
- if (released) return false;
40
- released = true;
41
- releaseSlot(slot);
42
- return true;
43
- },
44
- });
45
- }
46
-
47
- export function createDisplayPool({
48
- baseDisplay = DEFAULT_BASE_DISPLAY,
49
- maxConcurrent = DEFAULT_POOL_SIZE,
50
- } = {}) {
51
- const normalizedBaseDisplay = normalizePositiveInteger(baseDisplay, DEFAULT_BASE_DISPLAY);
52
- const normalizedPoolSize = normalizePositiveInteger(maxConcurrent, DEFAULT_POOL_SIZE);
53
- if (!Number.isFinite(normalizedBaseDisplay) || !Number.isFinite(normalizedPoolSize)) {
54
- throw createInvalidPoolError();
55
- }
56
-
57
- const slots = createSlots({
58
- baseDisplay: normalizedBaseDisplay,
59
- poolSize: normalizedPoolSize,
60
- });
61
- const waiters = [];
62
-
63
- function reserveAvailableSlot() {
64
- const slot = slots.find((candidate) => !candidate.inUse);
65
- if (!slot) return null;
66
- slot.inUse = true;
67
- return slot;
68
- }
69
-
70
- function dispatchWaiters() {
71
- while (waiters.length > 0) {
72
- const slot = reserveAvailableSlot();
73
- if (!slot) return;
74
- const waiter = waiters.shift();
75
- if (!waiter) return;
76
- if (waiter.timer) clearTimeout(waiter.timer);
77
- waiter.resolve(createLease(slot, releaseSlot));
78
- }
79
- }
80
-
81
- function releaseSlot(slot) {
82
- slot.inUse = false;
83
- dispatchWaiters();
84
- }
85
-
86
- async function acquireDisplay({ timeoutMs = null } = {}) {
87
- const slot = reserveAvailableSlot();
88
- if (slot) return createLease(slot, releaseSlot);
89
-
90
- return await new Promise((resolve, reject) => {
91
- const waiter = {
92
- resolve,
93
- reject,
94
- timer: null,
95
- };
96
- if (Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) > 0) {
97
- const normalizedTimeoutMs = Math.floor(Number(timeoutMs));
98
- waiter.timer = setTimeout(() => {
99
- const waiterIndex = waiters.indexOf(waiter);
100
- if (waiterIndex >= 0) waiters.splice(waiterIndex, 1);
101
- reject(createAcquireTimeoutError(normalizedTimeoutMs));
102
- }, normalizedTimeoutMs);
103
- }
104
- waiters.push(waiter);
105
- });
106
- }
107
-
108
- function snapshot() {
109
- return {
110
- baseDisplay: normalizedBaseDisplay,
111
- maxConcurrent: normalizedPoolSize,
112
- waiting: waiters.length,
113
- inUse: slots.filter(slot => slot.inUse).map(slot => slot.display),
114
- available: slots.filter(slot => !slot.inUse).map(slot => slot.display),
115
- };
116
- }
117
-
118
- return Object.freeze({
119
- baseDisplay: normalizedBaseDisplay,
120
- maxConcurrent: normalizedPoolSize,
121
- acquireDisplay,
122
- snapshot,
123
- });
124
- }
125
-
126
- export const defaultDisplayPool = createDisplayPool();
@@ -1,291 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { mkdirSync } from 'node:fs';
3
- import path from 'node:path';
4
-
5
- const DEFAULT_FPS = 30;
6
- const DEFAULT_WIDTH = 1080;
7
- const DEFAULT_HEIGHT = 1920;
8
- const DEFAULT_PRESET = 'veryfast';
9
- const DEFAULT_VIDEO_BITRATE = '4500k';
10
- const DEFAULT_VIDEO_MAX_RATE = '5500k';
11
- const DEFAULT_VIDEO_BUFFER_SIZE = '9000k';
12
-
13
- function normalizeInteger(value, fallback) {
14
- const parsed = Number.parseInt(String(value ?? ''), 10);
15
- if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
16
- return parsed;
17
- }
18
-
19
- function normalizeText(value) {
20
- if (typeof value !== 'string') return '';
21
- return value.trim();
22
- }
23
-
24
- function normalizeOutputPath(outputPath) {
25
- const normalized = normalizeText(outputPath);
26
- if (!normalized) {
27
- const error = new Error('ffmpeg_output_path_required');
28
- error.code = 'FFMPEG_OUTPUT_PATH_REQUIRED';
29
- throw error;
30
- }
31
- return path.resolve(normalized);
32
- }
33
-
34
- function normalizeDisplay(display) {
35
- const normalized = normalizeText(display);
36
- if (!/^:\d+$/.test(normalized)) {
37
- const error = new Error(`ffmpeg_display_invalid:${normalized || 'empty'}`);
38
- error.code = 'FFMPEG_DISPLAY_INVALID';
39
- throw error;
40
- }
41
- return normalized;
42
- }
43
-
44
- function appendLogChunk(current, chunk, maxLength = 8000) {
45
- const merged = `${current}${chunk}`;
46
- if (merged.length <= maxLength) return merged;
47
- return merged.slice(merged.length - maxLength);
48
- }
49
-
50
- function buildExitError({ name, code, signal, stderr = '', stdout = '' }) {
51
- const descriptor = `${name}_exited_unexpectedly:code=${code ?? 'null'}:signal=${signal ?? 'none'}`;
52
- const error = new Error(descriptor);
53
- error.code = `${String(name).toUpperCase()}_EXITED_UNEXPECTEDLY`;
54
- error.exitCode = code;
55
- error.signal = signal;
56
- error.stderr = stderr;
57
- error.stdout = stdout;
58
- return error;
59
- }
60
-
61
- export function buildFfmpegX11grabArgs({
62
- display,
63
- outputPath,
64
- width = DEFAULT_WIDTH,
65
- height = DEFAULT_HEIGHT,
66
- fps = DEFAULT_FPS,
67
- durationSec = null,
68
- preset = DEFAULT_PRESET,
69
- videoBitrate = DEFAULT_VIDEO_BITRATE,
70
- maxRate = DEFAULT_VIDEO_MAX_RATE,
71
- bufferSize = DEFAULT_VIDEO_BUFFER_SIZE,
72
- logLevel = 'warning',
73
- extraArgs = [],
74
- } = {}) {
75
- const normalizedDisplay = normalizeDisplay(display);
76
- const normalizedOutputPath = normalizeOutputPath(outputPath);
77
- const normalizedWidth = normalizeInteger(width, DEFAULT_WIDTH);
78
- const normalizedHeight = normalizeInteger(height, DEFAULT_HEIGHT);
79
- const normalizedFps = normalizeInteger(fps, DEFAULT_FPS);
80
-
81
- const args = [
82
- '-y',
83
- '-hide_banner',
84
- '-loglevel', normalizeText(logLevel) || 'warning',
85
- '-f', 'x11grab',
86
- '-draw_mouse', '1',
87
- '-framerate', String(normalizedFps),
88
- '-video_size', `${normalizedWidth}x${normalizedHeight}`,
89
- '-i', normalizedDisplay,
90
- ];
91
-
92
- const normalizedDurationSec = Number(durationSec);
93
- if (Number.isFinite(normalizedDurationSec) && normalizedDurationSec > 0) {
94
- args.push('-t', String(Math.ceil(normalizedDurationSec)));
95
- }
96
-
97
- args.push(
98
- '-c:v', 'libx264',
99
- '-preset', normalizeText(preset) || DEFAULT_PRESET,
100
- '-pix_fmt', 'yuv420p',
101
- '-b:v', normalizeText(videoBitrate) || DEFAULT_VIDEO_BITRATE,
102
- '-maxrate', normalizeText(maxRate) || DEFAULT_VIDEO_MAX_RATE,
103
- '-bufsize', normalizeText(bufferSize) || DEFAULT_VIDEO_BUFFER_SIZE,
104
- '-r', String(normalizedFps),
105
- '-movflags', '+faststart',
106
- );
107
-
108
- if (Array.isArray(extraArgs) && extraArgs.length > 0) {
109
- args.push(...extraArgs.map(item => String(item)));
110
- }
111
-
112
- args.push(normalizedOutputPath);
113
- return args;
114
- }
115
-
116
- export async function waitForProcessExit(child, timeoutMs = 5000) {
117
- if (!child || child.exitCode !== null) {
118
- return {
119
- code: child?.exitCode ?? 0,
120
- signal: child?.signalCode ?? null,
121
- timedOut: false,
122
- };
123
- }
124
-
125
- return await new Promise((resolve) => {
126
- const timer = setTimeout(() => {
127
- cleanup();
128
- resolve({
129
- code: null,
130
- signal: null,
131
- timedOut: true,
132
- });
133
- }, timeoutMs);
134
-
135
- const onExit = (code, signal) => {
136
- cleanup();
137
- resolve({
138
- code,
139
- signal,
140
- timedOut: false,
141
- });
142
- };
143
-
144
- const cleanup = () => {
145
- clearTimeout(timer);
146
- child.removeListener('exit', onExit);
147
- };
148
-
149
- child.on('exit', onExit);
150
- });
151
- }
152
-
153
- export async function stopFfmpegCapture(runner, {
154
- signal = 'SIGINT',
155
- timeoutMs = 10000,
156
- killTimeoutMs = 2000,
157
- } = {}) {
158
- const child = runner?.child;
159
- if (!child || child.exitCode !== null) return child?.exitCode ?? 0;
160
-
161
- child.kill(signal);
162
- const firstExit = await waitForProcessExit(child, timeoutMs);
163
- if (!firstExit.timedOut) return firstExit.code;
164
-
165
- child.kill('SIGKILL');
166
- const forceExit = await waitForProcessExit(child, killTimeoutMs);
167
- return forceExit.code;
168
- }
169
-
170
- export async function startFfmpegCapture({
171
- display,
172
- outputPath,
173
- width = DEFAULT_WIDTH,
174
- height = DEFAULT_HEIGHT,
175
- fps = DEFAULT_FPS,
176
- durationSec = null,
177
- preset = DEFAULT_PRESET,
178
- videoBitrate = DEFAULT_VIDEO_BITRATE,
179
- maxRate = DEFAULT_VIDEO_MAX_RATE,
180
- bufferSize = DEFAULT_VIDEO_BUFFER_SIZE,
181
- logLevel = 'warning',
182
- extraArgs = [],
183
- startupProbeMs = 1200,
184
- ffmpegBin = 'ffmpeg',
185
- env = {},
186
- } = {}) {
187
- const normalizedOutputPath = normalizeOutputPath(outputPath);
188
- mkdirSync(path.dirname(normalizedOutputPath), { recursive: true });
189
-
190
- const args = buildFfmpegX11grabArgs({
191
- display,
192
- outputPath: normalizedOutputPath,
193
- width,
194
- height,
195
- fps,
196
- durationSec,
197
- preset,
198
- videoBitrate,
199
- maxRate,
200
- bufferSize,
201
- logLevel,
202
- extraArgs,
203
- });
204
-
205
- let stdout = '';
206
- let stderr = '';
207
- let spawnError = null;
208
- const child = spawn(ffmpegBin, args, {
209
- env: {
210
- ...process.env,
211
- ...env,
212
- DISPLAY: normalizeDisplay(display),
213
- },
214
- stdio: ['ignore', 'pipe', 'pipe'],
215
- });
216
-
217
- child.stdout?.on('data', (chunk) => {
218
- stdout = appendLogChunk(stdout, String(chunk));
219
- });
220
- child.stderr?.on('data', (chunk) => {
221
- stderr = appendLogChunk(stderr, String(chunk));
222
- });
223
- child.once('error', (error) => {
224
- spawnError = error;
225
- });
226
-
227
- await new Promise(resolve => setTimeout(resolve, Math.max(0, Number(startupProbeMs) || 0)));
228
-
229
- if (spawnError) {
230
- const error = new Error(`ffmpeg_spawn_failed:${spawnError.message}`);
231
- error.code = 'FFMPEG_SPAWN_FAILED';
232
- throw error;
233
- }
234
-
235
- if (child.exitCode !== null) {
236
- throw buildExitError({
237
- name: 'ffmpeg',
238
- code: child.exitCode,
239
- signal: child.signalCode,
240
- stderr,
241
- stdout,
242
- });
243
- }
244
-
245
- const runner = {
246
- child,
247
- args,
248
- display: normalizeDisplay(display),
249
- outputPath: normalizedOutputPath,
250
- getStdout: () => stdout,
251
- getStderr: () => stderr,
252
- waitForExit: (timeoutMs) => waitForProcessExit(child, timeoutMs),
253
- stop: (options) => stopFfmpegCapture(runner, options),
254
- };
255
-
256
- return runner;
257
- }
258
-
259
- export function createUnexpectedExitWatcher(child, name) {
260
- let active = true;
261
- const normalizedName = normalizeText(name) || 'process';
262
- const promise = new Promise((resolve, reject) => {
263
- child.once('exit', (code, signal) => {
264
- if (!active) {
265
- resolve({ code, signal });
266
- return;
267
- }
268
- reject(buildExitError({
269
- name: normalizedName,
270
- code,
271
- signal,
272
- }));
273
- });
274
- child.once('error', (error) => {
275
- if (!active) {
276
- resolve({ code: null, signal: null });
277
- return;
278
- }
279
- const wrapped = new Error(`${normalizedName}_spawn_failed:${error.message}`);
280
- wrapped.code = `${normalizedName.toUpperCase()}_SPAWN_FAILED`;
281
- reject(wrapped);
282
- });
283
- });
284
-
285
- return {
286
- promise,
287
- deactivate() {
288
- active = false;
289
- },
290
- };
291
- }