2026-04-07 21:58:21 -04:00
|
|
|
|
import { execFile } from 'child_process';
|
|
|
|
|
|
import { promisify } from 'util';
|
|
|
|
|
|
|
|
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
|
|
|
|
|
|
|
|
export const runFFmpeg = async (args, options = {}) => {
|
|
|
|
|
|
const { stdio = 'pipe', ...otherOptions } = options;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { stdout, stderr } = await execFileAsync('ffmpeg', args, {
|
|
|
|
|
|
stdio,
|
|
|
|
|
|
...otherOptions,
|
|
|
|
|
|
});
|
|
|
|
|
|
return { stdout, stderr };
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
throw new Error(`FFmpeg error: ${error.message}\nStderr: ${error.stderr}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-16 00:29:49 -04:00
|
|
|
|
const runFFprobe = async (args) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { stdout } = await execFileAsync('ffprobe', args);
|
|
|
|
|
|
return { stdout };
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
throw new Error(`FFprobe error: ${error.message}\nStderr: ${error.stderr}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-07 21:58:21 -04:00
|
|
|
|
export const getMediaDuration = async (inputPath) => {
|
|
|
|
|
|
const args = [
|
|
|
|
|
|
'-v', 'error',
|
|
|
|
|
|
'-show_entries', 'format=duration',
|
2026-05-16 00:29:49 -04:00
|
|
|
|
'-of', 'default=noprint_wrappers=1:nokey=1',
|
2026-04-07 21:58:21 -04:00
|
|
|
|
inputPath,
|
|
|
|
|
|
];
|
2026-05-16 00:29:49 -04:00
|
|
|
|
const { stdout } = await runFFprobe(args);
|
2026-04-07 21:58:21 -04:00
|
|
|
|
return parseFloat(stdout.trim());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-18 23:22:26 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Return structured media metadata for an input file.
|
|
|
|
|
|
* Result shape:
|
|
|
|
|
|
* { fps, codec, resolution, durationMs, fileSizeBytes }
|
|
|
|
|
|
* Any field may be null if ffprobe cannot determine it.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const getMediaInfo = async (inputPath) => {
|
|
|
|
|
|
const args = [
|
|
|
|
|
|
'-v', 'quiet',
|
|
|
|
|
|
'-print_format', 'json',
|
|
|
|
|
|
'-show_streams',
|
|
|
|
|
|
'-show_format',
|
|
|
|
|
|
inputPath,
|
|
|
|
|
|
];
|
|
|
|
|
|
const { stdout } = await runFFprobe(args);
|
|
|
|
|
|
const info = JSON.parse(stdout);
|
|
|
|
|
|
|
|
|
|
|
|
const videoStream = (info.streams || []).find(s => s.codec_type === 'video');
|
|
|
|
|
|
const fmt = info.format || {};
|
|
|
|
|
|
|
|
|
|
|
|
let fps = null;
|
|
|
|
|
|
if (videoStream?.r_frame_rate) {
|
|
|
|
|
|
const [num, den] = videoStream.r_frame_rate.split('/').map(Number);
|
|
|
|
|
|
if (den > 0) fps = Math.round((num / den) * 1000) / 1000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(audio-tab): full audio track inspector with meters, mute/solo, faders
Issue #80 — replaces the stub AudioTab (two static waveforms) with a
broadcast-ops-grade audio panel:
- DB: add audio_metadata JSONB column to assets (migration 022)
- Worker: getMediaInfo now extracts per-stream audio metadata
(codec, channels, channel_layout, sample_rate, bit_depth, bit_rate,
language, title, disposition)
- Worker: proxy job persists audio_metadata into the assets row
- API: new GET /assets/:id/audio returns structured track list
- Frontend AudioTab: per-track rows with:
- Track name/index with language badge
- SVG waveform per track (color-coded)
- L/R level meters via Web Audio API AnalyserNode
- Per-track metadata row (codec, layout, sample rate, bit depth, bitrate)
- Mute / Solo buttons with proper solo-logic
- Per-track volume fader
- Master section with summed L/R meters and master fader
- MetadataTab: show audio track summary when audio_metadata present
- CSS: full audio-tab layout, responsive collapse at 900px
2026-05-27 00:53:52 -04:00
|
|
|
|
const audioStreams = (info.streams || []).filter(s => s.codec_type === 'audio');
|
|
|
|
|
|
const hasAudio = audioStreams.length > 0;
|
|
|
|
|
|
|
|
|
|
|
|
const audioMetadata = audioStreams.map(s => {
|
|
|
|
|
|
const bitDepth = s.bits_per_raw_sample
|
|
|
|
|
|
? parseInt(s.bits_per_raw_sample, 10)
|
|
|
|
|
|
: s.bit_depth
|
|
|
|
|
|
? parseInt(s.bit_depth, 10)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
return {
|
|
|
|
|
|
index: s.index ?? 0,
|
|
|
|
|
|
codec: s.codec_name || null,
|
|
|
|
|
|
channels: s.channels ? parseInt(s.channels, 10) : null,
|
|
|
|
|
|
channel_layout: s.channel_layout || null,
|
|
|
|
|
|
sample_rate: s.sample_rate ? parseInt(s.sample_rate, 10) : null,
|
|
|
|
|
|
bit_depth: bitDepth,
|
|
|
|
|
|
bit_rate: s.bit_rate ? parseInt(s.bit_rate, 10) : null,
|
|
|
|
|
|
language: s.tags?.language || null,
|
|
|
|
|
|
title: s.tags?.title || null,
|
|
|
|
|
|
disposition: s.disposition || {},
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
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
|
|
|
|
|
2026-05-18 23:22:26 -04:00
|
|
|
|
return {
|
|
|
|
|
|
fps,
|
feat(audio-tab): full audio track inspector with meters, mute/solo, faders
Issue #80 — replaces the stub AudioTab (two static waveforms) with a
broadcast-ops-grade audio panel:
- DB: add audio_metadata JSONB column to assets (migration 022)
- Worker: getMediaInfo now extracts per-stream audio metadata
(codec, channels, channel_layout, sample_rate, bit_depth, bit_rate,
language, title, disposition)
- Worker: proxy job persists audio_metadata into the assets row
- API: new GET /assets/:id/audio returns structured track list
- Frontend AudioTab: per-track rows with:
- Track name/index with language badge
- SVG waveform per track (color-coded)
- L/R level meters via Web Audio API AnalyserNode
- Per-track metadata row (codec, layout, sample rate, bit depth, bitrate)
- Mute / Solo buttons with proper solo-logic
- Per-track volume fader
- Master section with summed L/R meters and master fader
- MetadataTab: show audio track summary when audio_metadata present
- CSS: full audio-tab layout, responsive collapse at 900px
2026-05-27 00:53:52 -04:00
|
|
|
|
codec: videoStream?.codec_name || null,
|
|
|
|
|
|
resolution: videoStream ? `${videoStream.width}x${videoStream.height}` : null,
|
|
|
|
|
|
durationMs: fmt.duration ? Math.round(parseFloat(fmt.duration) * 1000) : null,
|
|
|
|
|
|
fileSizeBytes: fmt.size ? parseInt(fmt.size, 10) : null,
|
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
|
|
|
|
hasAudio,
|
feat(audio-tab): full audio track inspector with meters, mute/solo, faders
Issue #80 — replaces the stub AudioTab (two static waveforms) with a
broadcast-ops-grade audio panel:
- DB: add audio_metadata JSONB column to assets (migration 022)
- Worker: getMediaInfo now extracts per-stream audio metadata
(codec, channels, channel_layout, sample_rate, bit_depth, bit_rate,
language, title, disposition)
- Worker: proxy job persists audio_metadata into the assets row
- API: new GET /assets/:id/audio returns structured track list
- Frontend AudioTab: per-track rows with:
- Track name/index with language badge
- SVG waveform per track (color-coded)
- L/R level meters via Web Audio API AnalyserNode
- Per-track metadata row (codec, layout, sample rate, bit depth, bitrate)
- Mute / Solo buttons with proper solo-logic
- Per-track volume fader
- Master section with summed L/R meters and master fader
- MetadataTab: show audio track summary when audio_metadata present
- CSS: full audio-tab layout, responsive collapse at 900px
2026-05-27 00:53:52 -04:00
|
|
|
|
audioMetadata: audioMetadata.length > 0 ? audioMetadata : null,
|
2026-05-18 23:22:26 -04:00
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-07 21:58:21 -04:00
|
|
|
|
export const extractFrameAtTime = async (inputPath, outputPath, timeCode) => {
|
|
|
|
|
|
const args = [
|
|
|
|
|
|
'-ss', timeCode,
|
2026-05-16 00:29:49 -04:00
|
|
|
|
'-i', inputPath,
|
2026-04-07 21:58:21 -04:00
|
|
|
|
'-vframes', '1',
|
|
|
|
|
|
'-q:v', '2',
|
2026-05-16 00:29:49 -04:00
|
|
|
|
'-y',
|
2026-04-07 21:58:21 -04:00
|
|
|
|
outputPath,
|
|
|
|
|
|
];
|
|
|
|
|
|
await runFFmpeg(args);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
feat: SDK deployment UI, proxy encoding global settings, S3 env fallback
- Settings: drop AMPP tab, rename GPU/Transcoding → Proxy encoding
with explicit 'applied to every ingested file' wording, expose
CPU codec/preset options when GPU is off
- New Capture SDKs tab (Settings): upload Blackmagic / AJA / Deltacast
SDK archives (.zip / .tar.gz) staged to /sdk/<vendor>/ inside mam-api;
BMD is fully wired into the FFmpeg build pipeline, AJA + Deltacast
staging-only pending FFmpeg patches
- mam-api: new /api/v1/sdk routes (multer upload, extract, list, delete);
Dockerfile gets unzip+tar; docker-compose mounts /mnt/NVME/MAM/sdk:/sdk
- proxy worker now reads proxy-encoding settings from DB on every job,
builds args for libx264 / NVENC / VAAPI, falls back to libx264 on
hardware-encode failure
- settings GET /s3 falls back to S3_* env vars when DB is empty so the
UI reflects what's actually wired (fixes 'not configured' false alarm)
2026-05-22 22:58:32 -04:00
|
|
|
|
const NVENC_CODECS = new Set(['h264_nvenc', 'hevc_nvenc']);
|
|
|
|
|
|
const VAAPI_CODECS = new Set(['h264_vaapi', 'hevc_vaapi']);
|
|
|
|
|
|
const HW_CODECS = new Set([...NVENC_CODECS, ...VAAPI_CODECS]);
|
|
|
|
|
|
|
2026-04-07 21:58:21 -04:00
|
|
|
|
export const transcodeVideo = async (inputPath, outputPath, options = {}) => {
|
|
|
|
|
|
const {
|
feat: SDK deployment UI, proxy encoding global settings, S3 env fallback
- Settings: drop AMPP tab, rename GPU/Transcoding → Proxy encoding
with explicit 'applied to every ingested file' wording, expose
CPU codec/preset options when GPU is off
- New Capture SDKs tab (Settings): upload Blackmagic / AJA / Deltacast
SDK archives (.zip / .tar.gz) staged to /sdk/<vendor>/ inside mam-api;
BMD is fully wired into the FFmpeg build pipeline, AJA + Deltacast
staging-only pending FFmpeg patches
- mam-api: new /api/v1/sdk routes (multer upload, extract, list, delete);
Dockerfile gets unzip+tar; docker-compose mounts /mnt/NVME/MAM/sdk:/sdk
- proxy worker now reads proxy-encoding settings from DB on every job,
builds args for libx264 / NVENC / VAAPI, falls back to libx264 on
hardware-encode failure
- settings GET /s3 falls back to S3_* env vars when DB is empty so the
UI reflects what's actually wired (fixes 'not configured' false alarm)
2026-05-22 22:58:32 -04:00
|
|
|
|
videoCodec = 'libx264',
|
|
|
|
|
|
videoPreset = 'fast',
|
2026-05-26 13:44:18 -04:00
|
|
|
|
videoBitrate = '750k', // average/target for VBR
|
|
|
|
|
|
videoMinRate = null, // VBR minimum e.g. '500k'
|
|
|
|
|
|
videoMaxRate = null, // VBR maximum e.g. '1000k'
|
|
|
|
|
|
videoBufSize = null, // VBR buffer e.g. '2000k' (2× maxrate recommended)
|
|
|
|
|
|
rateControl = null, // 'cbr' | 'vbr' | 'cqp' — optional override for HW codecs
|
feat: SDK deployment UI, proxy encoding global settings, S3 env fallback
- Settings: drop AMPP tab, rename GPU/Transcoding → Proxy encoding
with explicit 'applied to every ingested file' wording, expose
CPU codec/preset options when GPU is off
- New Capture SDKs tab (Settings): upload Blackmagic / AJA / Deltacast
SDK archives (.zip / .tar.gz) staged to /sdk/<vendor>/ inside mam-api;
BMD is fully wired into the FFmpeg build pipeline, AJA + Deltacast
staging-only pending FFmpeg patches
- mam-api: new /api/v1/sdk routes (multer upload, extract, list, delete);
Dockerfile gets unzip+tar; docker-compose mounts /mnt/NVME/MAM/sdk:/sdk
- proxy worker now reads proxy-encoding settings from DB on every job,
builds args for libx264 / NVENC / VAAPI, falls back to libx264 on
hardware-encode failure
- settings GET /s3 falls back to S3_* env vars when DB is empty so the
UI reflects what's actually wired (fixes 'not configured' false alarm)
2026-05-22 22:58:32 -04:00
|
|
|
|
audioCodec = 'aac',
|
2026-05-26 13:44:18 -04:00
|
|
|
|
audioBitrate = '128k',
|
feat: SDK deployment UI, proxy encoding global settings, S3 env fallback
- Settings: drop AMPP tab, rename GPU/Transcoding → Proxy encoding
with explicit 'applied to every ingested file' wording, expose
CPU codec/preset options when GPU is off
- New Capture SDKs tab (Settings): upload Blackmagic / AJA / Deltacast
SDK archives (.zip / .tar.gz) staged to /sdk/<vendor>/ inside mam-api;
BMD is fully wired into the FFmpeg build pipeline, AJA + Deltacast
staging-only pending FFmpeg patches
- mam-api: new /api/v1/sdk routes (multer upload, extract, list, delete);
Dockerfile gets unzip+tar; docker-compose mounts /mnt/NVME/MAM/sdk:/sdk
- proxy worker now reads proxy-encoding settings from DB on every job,
builds args for libx264 / NVENC / VAAPI, falls back to libx264 on
hardware-encode failure
- settings GET /s3 falls back to S3_* env vars when DB is empty so the
UI reflects what's actually wired (fixes 'not configured' false alarm)
2026-05-22 22:58:32 -04:00
|
|
|
|
hasAudio = true,
|
2026-04-07 21:58:21 -04:00
|
|
|
|
} = options;
|
|
|
|
|
|
|
2026-05-26 13:44:18 -04:00
|
|
|
|
// libx264 / yuv420p require even dimensions.
|
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
|
|
|
|
const vf = "scale='trunc(iw/2)*2:trunc(ih/2)*2',format=yuv420p";
|
|
|
|
|
|
|
2026-04-07 21:58:21 -04:00
|
|
|
|
const args = [
|
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
|
|
|
|
'-analyzeduration', '100M',
|
|
|
|
|
|
'-probesize', '100M',
|
2026-04-07 21:58:21 -04:00
|
|
|
|
'-i', inputPath,
|
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
|
|
|
|
'-vf', vf,
|
2026-04-07 21:58:21 -04:00
|
|
|
|
'-c:v', videoCodec,
|
|
|
|
|
|
'-preset', videoPreset,
|
|
|
|
|
|
'-b:v', videoBitrate,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-05-26 13:44:18 -04:00
|
|
|
|
// VBR min/max/bufsize for libx264 ABR mode.
|
|
|
|
|
|
// When minrate+maxrate are set, libx264 operates in ABR with hard limits
|
|
|
|
|
|
// rather than strict CBR — quality varies per-scene within the envelope.
|
|
|
|
|
|
if (videoMinRate) args.push('-minrate', videoMinRate);
|
|
|
|
|
|
if (videoMaxRate) args.push('-maxrate', videoMaxRate);
|
|
|
|
|
|
if (videoBufSize) args.push('-bufsize', videoBufSize);
|
|
|
|
|
|
|
|
|
|
|
|
// NVENC/VAAPI hardware rate control flags
|
feat: SDK deployment UI, proxy encoding global settings, S3 env fallback
- Settings: drop AMPP tab, rename GPU/Transcoding → Proxy encoding
with explicit 'applied to every ingested file' wording, expose
CPU codec/preset options when GPU is off
- New Capture SDKs tab (Settings): upload Blackmagic / AJA / Deltacast
SDK archives (.zip / .tar.gz) staged to /sdk/<vendor>/ inside mam-api;
BMD is fully wired into the FFmpeg build pipeline, AJA + Deltacast
staging-only pending FFmpeg patches
- mam-api: new /api/v1/sdk routes (multer upload, extract, list, delete);
Dockerfile gets unzip+tar; docker-compose mounts /mnt/NVME/MAM/sdk:/sdk
- proxy worker now reads proxy-encoding settings from DB on every job,
builds args for libx264 / NVENC / VAAPI, falls back to libx264 on
hardware-encode failure
- settings GET /s3 falls back to S3_* env vars when DB is empty so the
UI reflects what's actually wired (fixes 'not configured' false alarm)
2026-05-22 22:58:32 -04:00
|
|
|
|
if (rateControl) {
|
|
|
|
|
|
if (NVENC_CODECS.has(videoCodec)) {
|
|
|
|
|
|
args.push('-rc', rateControl);
|
|
|
|
|
|
} else if (VAAPI_CODECS.has(videoCodec)) {
|
|
|
|
|
|
args.push('-rc_mode', rateControl.toUpperCase());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
if (hasAudio) {
|
|
|
|
|
|
args.push('-c:a', audioCodec, '-b:a', audioBitrate);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
args.push('-an');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
args.push('-movflags', '+faststart', '-y', outputPath);
|
|
|
|
|
|
|
2026-04-07 21:58:21 -04:00
|
|
|
|
await runFFmpeg(args);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
feat: SDK deployment UI, proxy encoding global settings, S3 env fallback
- Settings: drop AMPP tab, rename GPU/Transcoding → Proxy encoding
with explicit 'applied to every ingested file' wording, expose
CPU codec/preset options when GPU is off
- New Capture SDKs tab (Settings): upload Blackmagic / AJA / Deltacast
SDK archives (.zip / .tar.gz) staged to /sdk/<vendor>/ inside mam-api;
BMD is fully wired into the FFmpeg build pipeline, AJA + Deltacast
staging-only pending FFmpeg patches
- mam-api: new /api/v1/sdk routes (multer upload, extract, list, delete);
Dockerfile gets unzip+tar; docker-compose mounts /mnt/NVME/MAM/sdk:/sdk
- proxy worker now reads proxy-encoding settings from DB on every job,
builds args for libx264 / NVENC / VAAPI, falls back to libx264 on
hardware-encode failure
- settings GET /s3 falls back to S3_* env vars when DB is empty so the
UI reflects what's actually wired (fixes 'not configured' false alarm)
2026-05-22 22:58:32 -04:00
|
|
|
|
export const isHwCodec = (codec) => HW_CODECS.has(codec);
|
|
|
|
|
|
|
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
|
|
|
|
// Single-frame poster — used as a fallback "proxy" for still-image assets
|
|
|
|
|
|
// so the library can show them without a transcoded video.
|
|
|
|
|
|
export const transcodeImage = async (inputPath, outputPath) => {
|
|
|
|
|
|
await runFFmpeg([
|
|
|
|
|
|
'-i', inputPath,
|
|
|
|
|
|
'-vf', "scale='min(1920,iw)':-2",
|
|
|
|
|
|
'-q:v', '3',
|
|
|
|
|
|
'-y', outputPath,
|
|
|
|
|
|
]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-07 21:58:21 -04:00
|
|
|
|
export const trimSegment = async (inputPath, outputPath, inPoint, outPoint) => {
|
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
|
|
|
|
const info = await getMediaInfo(inputPath);
|
|
|
|
|
|
const fps = info.fps || 30;
|
|
|
|
|
|
const inSeconds = inPoint / fps;
|
|
|
|
|
|
const frameCount = outPoint - inPoint;
|
|
|
|
|
|
|
2026-05-28 14:32:49 -04:00
|
|
|
|
// `-frames:v` bounds the video output but says nothing about audio. Without
|
|
|
|
|
|
// -c:a copy + -shortest the audio track is either dropped or runs past
|
|
|
|
|
|
// the trimmed video. We need audio in the final concat output, so passthrough
|
|
|
|
|
|
// the audio codec and stop on the shortest stream.
|
2026-04-07 21:58:21 -04:00
|
|
|
|
const args = [
|
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
|
|
|
|
'-ss', inSeconds.toString(),
|
2026-04-07 21:58:21 -04:00
|
|
|
|
'-i', inputPath,
|
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
|
|
|
|
'-frames:v', frameCount.toString(),
|
2026-05-28 14:32:49 -04:00
|
|
|
|
'-c:a', 'copy',
|
|
|
|
|
|
'-shortest',
|
feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests)
- #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation,
conform job submission with progress polling via BullMQ
- #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint,
trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via
ExtendScript, temp segment signed URL endpoint
- #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens
(accent oklch(45% 0.20 266)), slide panels, preset cards, chip components
- #34 Cleanup Task: existing task validated and properly registered
- #35 Testing: comprehensive 33-scenario E2E test plan
- #36 Documentation: advanced features guide with workflows, troubleshooting,
presets table, and architecture overview
- #24 PR merge: verified mergeable
All server endpoints, worker queues, and ExtendScript functions wired together
2026-05-24 13:19:24 -04:00
|
|
|
|
'-start_number', '0',
|
2026-05-16 00:29:49 -04:00
|
|
|
|
'-y',
|
2026-04-07 21:58:21 -04:00
|
|
|
|
outputPath,
|
|
|
|
|
|
];
|
|
|
|
|
|
await runFFmpeg(args);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 22:38:42 -04:00
|
|
|
|
// Segment an existing MP4/MOV into HLS (fMP4) — init.mp4 + segment_*.m4s +
|
|
|
|
|
|
// playlist.m3u8. Keeps the original codec (no re-encode) so this is cheap to
|
|
|
|
|
|
// run after the proxy transcode. fMP4 segments stay <5 MB at our proxy
|
|
|
|
|
|
// bitrate, which sidesteps RustFS's broken byte-range path on large objects.
|
|
|
|
|
|
export const segmentToHls = async (inputPath, outputDir, options = {}) => {
|
|
|
|
|
|
const {
|
|
|
|
|
|
segmentDurationSec = 4,
|
|
|
|
|
|
playlistName = 'playlist.m3u8',
|
|
|
|
|
|
initName = 'init.mp4',
|
|
|
|
|
|
segmentPattern = 'segment_%05d.m4s',
|
|
|
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
|
|
|
const args = [
|
|
|
|
|
|
'-i', inputPath,
|
|
|
|
|
|
'-c', 'copy',
|
|
|
|
|
|
'-f', 'hls',
|
|
|
|
|
|
'-hls_time', String(segmentDurationSec),
|
|
|
|
|
|
'-hls_playlist_type', 'vod',
|
|
|
|
|
|
'-hls_segment_type', 'fmp4',
|
|
|
|
|
|
'-hls_flags', 'independent_segments',
|
|
|
|
|
|
'-hls_fmp4_init_filename', initName,
|
|
|
|
|
|
'-hls_segment_filename', `${outputDir}/${segmentPattern}`,
|
|
|
|
|
|
'-y',
|
|
|
|
|
|
`${outputDir}/${playlistName}`,
|
|
|
|
|
|
];
|
|
|
|
|
|
await runFFmpeg(args);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-07 21:58:21 -04:00
|
|
|
|
export const concatSegments = async (segmentListFile, outputPath) => {
|
|
|
|
|
|
const args = [
|
|
|
|
|
|
'-f', 'concat',
|
|
|
|
|
|
'-safe', '0',
|
|
|
|
|
|
'-i', segmentListFile,
|
|
|
|
|
|
'-c', 'copy',
|
2026-05-16 00:29:49 -04:00
|
|
|
|
'-y',
|
2026-04-07 21:58:21 -04:00
|
|
|
|
outputPath,
|
|
|
|
|
|
];
|
|
|
|
|
|
await runFFmpeg(args);
|
|
|
|
|
|
};
|