Skip to main content

rivet/encoder_worker/
cmaf_worker.rs

1//! CMAF segment worker: encodes one chunk and writes one CMAF segment file.
2
3use anyhow::{Context, Result};
4use std::sync::Arc;
5use tokio::sync::mpsc;
6use codec::encode::{self, EncoderConfig};
7use codec::frame::ColorMetadata;
8use container::cmaf::{CmafVideoMuxer, CmafVideoMuxerOptions, SegmentInfo};
9use crate::cmaf_util::add_packet_with_segment_flush;
10use crate::frame_queue::{SegmentChunk, SegmentChunkQueue};
11use super::{EncoderWorkerConfig, WorkerOutput, InvariantCheck, validate_or_set_rung_invariant};
12
13/// Run the encoder loop until the chunk queue is closed and drained.
14/// Designed to be wrapped in `tokio::task::spawn_blocking`.
15///
16/// `progress_tx` receives the shared cumulative `frames_encoded_total`
17/// after every encoded frame; the caller's drain task fires wire
18/// events from this stream. Multiple workers bump the same counter,
19/// so the progress reading stays monotonic across worker handoffs.
20#[allow(clippy::too_many_arguments)]
21pub fn run_encoder_worker_blocking(
22    cfg: EncoderWorkerConfig,
23    queue: Arc<SegmentChunkQueue>,
24    rt: tokio::runtime::Handle,
25    shared_frames_encoded: Arc<std::sync::atomic::AtomicU64>,
26    progress_tx: mpsc::Sender<u64>,
27) -> Result<WorkerOutput> {
28    let enc_config = super::build_enc_config(&cfg);
29    let encoder_color_metadata = cfg.output_color_metadata;
30
31    let mut segments_written: Vec<SegmentInfo> = Vec::new();
32    let mut init_segment_written = false;
33
34    tracing::debug!(rung_idx = cfg.rung_idx, gpu_index = ?cfg.gpu_index, "encoder worker started; awaiting first chunk");
35    loop {
36        let chunk = match rt.block_on(queue.pop()) {
37            Some(c) => c,
38            None => break,
39        };
40        tracing::debug!(rung_idx = cfg.rung_idx, segment = chunk.segment_idx, frames = chunk.frames.len(), "encoder worker popped chunk");
41        match encode_one_segment(
42            &cfg,
43            &enc_config,
44            encoder_color_metadata,
45            chunk,
46            &mut init_segment_written,
47            &shared_frames_encoded,
48            &progress_tx,
49        )? {
50            SegmentOutcome::Wrote {
51                info,
52                segment_idx,
53                frames,
54            } => {
55                let role = if segment_idx == 0 {
56                    "primary"
57                } else {
58                    "worker"
59                };
60                tracing::info!(
61                    rung_idx = cfg.rung_idx,
62                    gpu_index = ?cfg.gpu_index,
63                    role,
64                    segment = segment_idx,
65                    frames_encoded = frames,
66                    "rung segment flushed",
67                );
68                segments_written.push(info);
69            }
70            SegmentOutcome::RequeuedOnMismatch {
71                chunk: rejected,
72                diff,
73            } => {
74                // Helper from a vendor whose AV1 sequence header diverges
75                // from the rung's invariant on mandatory fields. Put the
76                // chunk back at the head of the queue so a matching-vendor
77                // worker (always at least the initial worker) picks it up.
78                // Exit clean — the run completes without this helper.
79                tracing::warn!(
80                    rung_idx = cfg.rung_idx,
81                    gpu_index = ?cfg.gpu_index,
82                    gpu_vendor = ?cfg.gpu_vendor,
83                    rejected_segment = rejected.segment_idx,
84                    diff = %diff,
85                    "encoder worker: codec invariant mismatch on first packet — \
86                     requeuing chunk for a matching-vendor worker and exiting",
87                );
88                let _ = queue.push_front(rejected);
89                break;
90            }
91        }
92    }
93
94    Ok(WorkerOutput {
95        gpu_index: cfg.gpu_index,
96        segments: segments_written,
97    })
98}
99
100/// Outcome of an `encode_one_segment` call. `Wrote` is the happy
101/// path; `RequeuedOnMismatch` returns the chunk verbatim so the outer
102/// loop can put it back at the head of the queue for another worker.
103enum SegmentOutcome {
104    Wrote {
105        info: SegmentInfo,
106        segment_idx: usize,
107        frames: usize,
108    },
109    RequeuedOnMismatch {
110        chunk: SegmentChunk,
111        diff: String,
112    },
113}
114
115fn encode_one_segment(
116    cfg: &EncoderWorkerConfig,
117    enc_config: &EncoderConfig,
118    encoder_color_metadata: ColorMetadata,
119    chunk: SegmentChunk,
120    init_segment_written: &mut bool,
121    shared_frames_encoded: &std::sync::atomic::AtomicU64,
122    progress_tx: &mpsc::Sender<u64>,
123) -> Result<SegmentOutcome> {
124    let write_init = chunk.segment_idx == 0 && !*init_segment_written;
125    let muxer_options = CmafVideoMuxerOptions {
126        first_segment_index: (chunk.segment_idx as u32) + 1,
127        first_segment_base_decode_time: chunk.segment_idx as u64 * cfg.segment_target_ticks,
128        write_init_segment: write_init,
129    };
130    let mut muxer = CmafVideoMuxer::new_with_codec_options(
131        &cfg.output_dir,
132        cfg.width,
133        cfg.height,
134        cfg.timescale,
135        encoder_color_metadata,
136        cfg.codec,
137        muxer_options,
138    )
139    .with_context(|| {
140        format!(
141            "creating CmafVideoMuxer for segment {} in {}",
142            chunk.segment_idx,
143            cfg.output_dir.display()
144        )
145    })?;
146
147    let mut encoder =
148        encode::select_encoder(enc_config.clone(), None).context("creating encoder for segment")?;
149
150    // Buffered packets emitted from the encoder, awaiting either
151    // commit-to-muxer (after invariant validation passes) or discard
152    // (on mismatch). The first packet's bytes are the AV1 sequence
153    // header OBU that we feed to the invariant validator.
154    let mut pending_packets: Vec<codec::encode::EncodedPacket> = Vec::new();
155    let mut first_packet_decision: Option<bool> = None; // None=undecided, Some(true)=commit, Some(false)=reject
156
157    let segment_idx = chunk.segment_idx;
158    let frame_count = chunk.frames.len();
159
160    for frame in &chunk.frames {
161        encoder
162            .send_frame(frame)
163            .context("encoder.send_frame in worker")?;
164        while let Some(packet) = encoder
165            .receive_packet()
166            .context("encoder.receive_packet in worker")?
167        {
168            if first_packet_decision.is_none() {
169                match validate_or_set_rung_invariant(
170                    cfg.rung_idx,
171                    cfg.gpu_vendor,
172                    &cfg.rung_invariant,
173                    &packet.data,
174                    cfg.codec,
175                )? {
176                    InvariantCheck::Matched | InvariantCheck::SetByThisWorker => {
177                        first_packet_decision = Some(true);
178                    }
179                    InvariantCheck::Mismatched { diff } => {
180                        // Discard everything in flight. The muxer hasn't
181                        // flushed any segment yet (first packet of a
182                        // chunk is far below the segment-duration target),
183                        // and init.mp4 is only written by finalize() —
184                        // which we don't call. Drop muxer + encoder
185                        // implicitly when we return.
186                        return Ok(SegmentOutcome::RequeuedOnMismatch { chunk, diff });
187                    }
188                }
189                pending_packets.push(packet);
190                continue;
191            }
192            // first_packet_decision == Some(true): commit
193            // First drain any buffered packets we held back during
194            // validation.
195            if !pending_packets.is_empty() {
196                for held in pending_packets.drain(..) {
197                    add_packet_with_segment_flush(
198                        &mut muxer,
199                        &held,
200                        cfg.per_frame_ticks,
201                        cfg.segment_target_ticks,
202                    )
203                    .context("CMAF segment-flush add (held)")?;
204                }
205            }
206            add_packet_with_segment_flush(
207                &mut muxer,
208                &packet,
209                cfg.per_frame_ticks,
210                cfg.segment_target_ticks,
211            )
212            .context("CMAF segment-flush add (worker)")?;
213        }
214        let n = shared_frames_encoded.fetch_add(1, std::sync::atomic::Ordering::AcqRel) + 1;
215        let _ = progress_tx.try_send(n);
216    }
217
218    // Drain remaining held packets (e.g. if the only packets emitted
219    // were buffered during the single validation step).
220    if first_packet_decision == Some(true) && !pending_packets.is_empty() {
221        for held in pending_packets.drain(..) {
222            add_packet_with_segment_flush(
223                &mut muxer,
224                &held,
225                cfg.per_frame_ticks,
226                cfg.segment_target_ticks,
227            )
228            .context("CMAF segment-flush add (final-held)")?;
229        }
230    }
231
232    encoder.flush().context("encoder.flush in worker")?;
233    while let Some(packet) = encoder
234        .receive_packet()
235        .context("encoder.receive_packet after flush")?
236    {
237        add_packet_with_segment_flush(
238            &mut muxer,
239            &packet,
240            cfg.per_frame_ticks,
241            cfg.segment_target_ticks,
242        )
243        .context("CMAF segment-flush add post-flush (worker)")?;
244    }
245
246    let manifest = muxer
247        .finalize()
248        .context("finalize CmafVideoMuxer (per-segment worker)")?;
249
250    if write_init {
251        *init_segment_written = true;
252    }
253
254    let info = manifest
255        .segments
256        .last()
257        .ok_or_else(|| {
258            anyhow::anyhow!(
259                "encoder worker produced no segment for chunk idx {} (rung {}, gpu {:?}); \
260                 frames in chunk = {}",
261                segment_idx,
262                cfg.rung_idx,
263                cfg.gpu_index,
264                frame_count,
265            )
266        })?
267        .clone();
268    Ok(SegmentOutcome::Wrote {
269        info,
270        segment_idx,
271        frames: frame_count,
272    })
273}