fix(playout): non-fatal consumer + loadPlaylist guard
- startChannel: make primary consumer ADD non-fatal. CasparCG decodes and routes media without an output consumer, so NDI channels (no SDK) and misconfigured SRT/RTMP channels still load/play clips and expose the HLS preview. state.lastError carries the consumer error for UI visibility without blocking operation. - loadPlaylist: throw early if state.running=false (channel/start was never called or failed hard) with a clear error instead of a cryptic CasparCG AMCP error propagating to the operator. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e8f91cf4b4
commit
00b04aa4a8
1 changed files with 25 additions and 6 deletions
|
|
@ -111,22 +111,38 @@ export class PlayoutManager {
|
|||
// Start the channel: bring up CasparCG's primary output consumer for the
|
||||
// target, plus a second FFMPEG consumer writing HLS for the UI preview
|
||||
// monitor (~4-6s lag, reuses capture's /live/<id> plumbing).
|
||||
//
|
||||
// The primary consumer failure is NON-FATAL. CasparCG can decode and route
|
||||
// media through its pipeline even without an output consumer. This means:
|
||||
// - NDI channels work (load/play/transport) even if libndi.so is absent.
|
||||
// - SRT/RTMP channels work even if the destination URL is unreachable.
|
||||
// - The HLS preview consumer is always attempted independently.
|
||||
//
|
||||
// state.consumerError is set when the primary consumer fails so the mam-api
|
||||
// can surface a warning in the channel status without blocking operation.
|
||||
async startChannel({ outputType, outputConfig = {}, videoFormat = '1080p5994' }) {
|
||||
await this.amcp.waitReady(30000);
|
||||
|
||||
// Set the channel video mode, then attach the output consumer.
|
||||
// Set the channel video mode first.
|
||||
try { await this.amcp.send(`SET ${CHANNEL} MODE ${videoFormat}`); }
|
||||
catch (err) { console.warn(`[playout] SET MODE failed (continuing): ${err.message}`); }
|
||||
|
||||
const consumer = await this._consumerCommand(outputType, outputConfig);
|
||||
await this.amcp.send(`ADD ${CHANNEL} ${consumer}`);
|
||||
// Primary output consumer — non-fatal.
|
||||
let consumerError = null;
|
||||
try {
|
||||
const consumer = await this._consumerCommand(outputType, outputConfig);
|
||||
await this.amcp.send(`ADD ${CHANNEL} ${consumer}`);
|
||||
} catch (err) {
|
||||
consumerError = err.message;
|
||||
console.warn(`[playout] primary consumer ADD failed (continuing without output): ${err.message}`);
|
||||
}
|
||||
|
||||
// HLS preview consumer — always attempt, independently non-fatal.
|
||||
if (HLS_DIR) {
|
||||
try {
|
||||
await this._addHlsConsumer();
|
||||
console.log(`[playout] HLS preview at ${HLS_DIR}/index.m3u8`);
|
||||
} catch (err) {
|
||||
// HLS preview is non-fatal — operators still get the on-air output.
|
||||
console.warn(`[playout] HLS preview consumer failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -137,8 +153,8 @@ export class PlayoutManager {
|
|||
this.state.videoFormat = videoFormat;
|
||||
this.state.fps = fpsFor(videoFormat);
|
||||
this.state.startedAt = new Date().toISOString();
|
||||
this.state.lastError = null;
|
||||
console.log(`[playout] channel started output=${outputType} mode=${videoFormat} fps=${this.state.fps.toFixed(3)}`);
|
||||
this.state.lastError = consumerError;
|
||||
console.log(`[playout] channel started output=${outputType} mode=${videoFormat} fps=${this.state.fps.toFixed(3)}${consumerError ? ' ⚠ consumer: ' + consumerError : ''}`);
|
||||
return this.getStatus();
|
||||
}
|
||||
|
||||
|
|
@ -181,6 +197,9 @@ export class PlayoutManager {
|
|||
// Load a playlist (array of { id, asset_id, media_path, in_point, out_point,
|
||||
// transition, transition_ms, clip_name }) and start playing from index 0.
|
||||
async loadPlaylist({ items = [], loop = false }) {
|
||||
if (!this.state.running) {
|
||||
throw new Error('Channel not started — call /channel/start first');
|
||||
}
|
||||
this.state.playlist = items;
|
||||
this.state.loop = !!loop;
|
||||
this.state.currentIndex = -1;
|
||||
|
|
|
|||
Loading…
Reference in a new issue