2026-05-31 14:50:31 -04:00
|
|
|
import { spawn, execFileSync } from 'child_process';
|
2026-05-31 18:14:59 -04:00
|
|
|
import { mkdirSync, writeFileSync, createReadStream, statSync, unlinkSync } from 'node:fs';
|
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
|
|
|
import { dirname } from 'node:path';
|
2026-04-07 21:58:29 -04:00
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
|
import { createUploadStream } from './s3/client.js';
|
|
|
|
|
|
2026-06-01 07:53:12 -04:00
|
|
|
/**
|
2026-06-01 19:07:15 -04:00
|
|
|
* Reads stderr lines from a spawned process until it finds a JSON line.
|
|
|
|
|
* Non-JSON lines (e.g. [board] log messages from the bridge) are logged
|
|
|
|
|
* and skipped. Resolves with the parsed JSON object when a JSON line arrives.
|
|
|
|
|
* Rejects if the process exits before emitting JSON, or if timeoutMs elapses.
|
2026-06-01 07:53:12 -04:00
|
|
|
*/
|
|
|
|
|
function readFirstStderrLine(proc, timeoutMs = 35_000) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
let buf = '';
|
|
|
|
|
let settled = false;
|
|
|
|
|
const settle = (fn) => { if (settled) return; settled = true; fn(); };
|
|
|
|
|
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
settle(() => reject(new Error(`deltacast-capture: timed out waiting for format JSON after ${timeoutMs}ms`)));
|
|
|
|
|
}, timeoutMs);
|
|
|
|
|
|
|
|
|
|
proc.stderr.setEncoding('utf8');
|
|
|
|
|
proc.stderr.on('data', (chunk) => {
|
|
|
|
|
buf += chunk;
|
2026-06-01 19:07:15 -04:00
|
|
|
let nl;
|
|
|
|
|
// Process all complete lines in the buffer
|
|
|
|
|
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
|
|
|
const line = buf.slice(0, nl).trim();
|
|
|
|
|
buf = buf.slice(nl + 1);
|
|
|
|
|
if (!line) continue;
|
|
|
|
|
// Skip non-JSON log lines emitted by the bridge (e.g. "[board] waiting...")
|
|
|
|
|
if (!line.startsWith('{')) {
|
|
|
|
|
console.error(`[deltacast-bridge] ${line}`);
|
|
|
|
|
continue;
|
2026-06-01 07:53:12 -04:00
|
|
|
}
|
2026-06-01 19:07:15 -04:00
|
|
|
clearTimeout(timer);
|
2026-05-31 18:38:56 -04:00
|
|
|
try {
|
2026-06-01 19:07:15 -04:00
|
|
|
const parsed = JSON.parse(line);
|
|
|
|
|
if (parsed.error) {
|
|
|
|
|
settle(() => reject(new Error(`deltacast-capture: ${parsed.error}`)));
|
2026-05-31 18:38:56 -04:00
|
|
|
} else {
|
2026-06-01 19:07:15 -04:00
|
|
|
settle(() => resolve(parsed));
|
2026-05-31 18:38:56 -04:00
|
|
|
}
|
2026-06-01 19:07:15 -04:00
|
|
|
} catch (e) {
|
|
|
|
|
settle(() => reject(new Error(`deltacast-capture: invalid JSON on stderr: ${line}`)));
|
2026-05-16 08:19:41 -04:00
|
|
|
}
|
2026-06-01 19:07:15 -04:00
|
|
|
return;
|
2026-05-17 07:39:19 -04:00
|
|
|
}
|
2026-05-16 08:19:41 -04:00
|
|
|
});
|
|
|
|
|
|
2026-06-01 19:07:15 -04:00
|
|
|
proc.on('exit', (code) => {
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
settle(() => reject(new Error(`deltacast-capture: exited with code ${code} before emitting format JSON`)));
|
2026-05-31 18:14:59 -04:00
|
|
|
});
|
2026-06-01 19:07:15 -04:00
|
|
|
});
|
2026-04-07 21:58:29 -04:00
|
|
|
}
|