#[cfg(feature = "hwencode")]
use std::sync::Condvar;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SyncSender, TryRecvError, TrySendError};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use crate::audio::audio_block_bytes;
#[cfg(test)]
use crate::heartbeat::InjectedStall;
use crate::heartbeat::{CommandProgress, STALL_POLL, WorkerHeartbeat};
use crate::metrics::{ExportMetrics, frames_per_second};
use crate::protocol::HostError;
use alpha2_media::{CancelToken, EncodeEvent, EncodeSink};
use alpha2_native::{
EncodeStageTimings, NativeAvEncodeSink, NativeVideoEncodeSink, NativeVideoEncoderSelection,
Yuv420pFrameBuffer,
};
use alpha2_time::MediaTime;
pub(crate) fn encode_failure(step: &str, error: impl std::error::Error + 'static) -> String {
encode_error(format!(
"native encode failed while {step}: {}",
crate::error_message(error)
))
}
pub(crate) fn encode_error(message: impl Into<String>) -> String {
crate::protocol::code_tag("ENCODE_ERROR", message)
}
pub struct PooledFrame {
frame: Option<Yuv420pFrameBuffer>,
pool: Arc<FramePool>,
}
impl PooledFrame {
pub fn buffer_mut(&mut self) -> &mut Yuv420pFrameBuffer {
self.frame
.as_mut()
.expect("a pooled frame is present for the whole loan")
}
}
impl Drop for PooledFrame {
fn drop(&mut self) {
if let Some(frame) = self.frame.take() {
self.pool.put(frame);
}
}
}
#[derive(Debug)]
pub struct FramePool {
idle: Mutex<Vec<Yuv420pFrameBuffer>>,
width: i32,
height: i32,
allocated: AtomicU64,
}
impl FramePool {
pub fn new(width: u32, height: u32) -> Result<Arc<Self>, String> {
Ok(Arc::new(Self {
idle: Mutex::new(Vec::new()),
width: i32::try_from(width).map_err(|_| "output width does not fit".to_owned())?,
height: i32::try_from(height).map_err(|_| "output height does not fit".to_owned())?,
allocated: AtomicU64::new(0),
}))
}
pub fn take(self: &Arc<Self>) -> Result<PooledFrame, String> {
let idle = self
.idle
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.pop();
let frame = if let Some(frame) = idle {
frame
} else {
self.allocated.fetch_add(1, Ordering::Relaxed);
Yuv420pFrameBuffer::new(self.width, self.height).map_err(|error| {
encode_error(format!("cannot allocate an encoder frame: {error}"))
})?
};
Ok(PooledFrame {
frame: Some(frame),
pool: Arc::clone(self),
})
}
pub fn allocated(&self) -> u64 {
self.allocated.load(Ordering::Relaxed)
}
fn put(&self, frame: Yuv420pFrameBuffer) {
self.idle
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(frame);
}
}
#[cfg(feature = "hwencode")]
#[derive(Debug)]
pub struct GpuFrameBound {
live: Mutex<usize>,
released: Condvar,
limit: usize,
peak: AtomicU64,
}
#[cfg(feature = "hwencode")]
impl GpuFrameBound {
pub fn new(limit: usize) -> Arc<Self> {
Arc::new(Self {
live: Mutex::new(0),
released: Condvar::new(),
limit: limit.max(1),
peak: AtomicU64::new(0),
})
}
pub fn acquire(
self: &Arc<Self>,
cancel: &CancelToken,
stalled: &dyn Fn() -> Result<(), String>,
acquire: impl FnOnce() -> Result<alpha2_native::EncoderSurface, alpha2_media::EncodeError>,
) -> Result<GpuFrameLease, String> {
let mut live = self
.live
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while *live >= self.limit {
if cancel.is_cancelled() {
return Err(encode_error("native encode was cancelled"));
}
stalled()?;
let (guard, _) = self
.released
.wait_timeout(live, crate::WORKER_RETRY_INTERVAL)
.unwrap_or_else(std::sync::PoisonError::into_inner);
live = guard;
}
*live = live.saturating_add(1);
self.peak
.fetch_max(u64::try_from(*live).unwrap_or(u64::MAX), Ordering::Relaxed);
drop(live);
let surface = acquire().map_err(|error| {
self.release();
encode_error(format!("cannot allocate an encoder surface: {error}"))
})?;
Ok(GpuFrameLease {
surface: Some(surface),
bound: Arc::clone(self),
})
}
pub fn peak(&self) -> u64 {
self.peak.load(Ordering::Relaxed)
}
fn release(&self) {
let mut live = self
.live
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*live = live.saturating_sub(1);
drop(live);
self.released.notify_one();
}
}
#[cfg(feature = "hwencode")]
pub struct GpuFrameLease {
surface: Option<alpha2_native::EncoderSurface>,
bound: Arc<GpuFrameBound>,
}
#[cfg(feature = "hwencode")]
impl GpuFrameLease {
pub fn surface(&self) -> &alpha2_native::EncoderSurface {
self.surface
.as_ref()
.expect("a leased surface is present for the whole loan")
}
pub fn surface_mut(&mut self) -> &mut alpha2_native::EncoderSurface {
self.surface
.as_mut()
.expect("a leased surface is present for the whole loan")
}
}
#[cfg(feature = "hwencode")]
impl Drop for GpuFrameLease {
fn drop(&mut self) {
self.surface = None;
self.bound.release();
}
}
#[allow(
clippy::large_enum_variant,
reason = "one sink per export, moved once into its worker"
)]
pub enum NativeExportSink {
AudioVideo(NativeAvEncodeSink),
Video(NativeVideoEncodeSink),
#[cfg(feature = "hwencode")]
GpuVideo(alpha2_native::HardwareEncodeSession),
}
impl NativeExportSink {
pub fn video_selection(&self) -> &NativeVideoEncoderSelection {
match self {
Self::AudioVideo(sink) => sink.video_selection(),
Self::Video(sink) => sink.selection(),
#[cfg(feature = "hwencode")]
Self::GpuVideo(session) => session.selection(),
}
}
pub fn is_hardware(&self) -> bool {
#[cfg(feature = "hwencode")]
if matches!(self, Self::GpuVideo(_)) {
return true;
}
self.video_selection().hardware_accelerated
}
pub fn stage_timings(&self) -> EncodeStageTimings {
match self {
Self::AudioVideo(sink) => sink.stage_timings(),
Self::Video(sink) => sink.stage_timings(),
#[cfg(feature = "hwencode")]
Self::GpuVideo(session) => session.stage_timings(),
}
}
pub fn write(&mut self, event: EncodeEvent, cancel: &CancelToken) -> Result<(), String> {
#[cfg(feature = "hwencode")]
if let Self::GpuVideo(session) = self {
if cancel.is_cancelled() {
return Err(encode_error("native encode was cancelled"));
}
let EncodeEvent::Audio(block) = &event else {
return Err(encode_error(
"the GPU encode path does not take software video frames",
));
};
return session
.write_audio(block)
.map_err(|error| encode_failure("writing media", error));
}
match self {
Self::AudioVideo(sink) => sink.write(event, cancel),
Self::Video(sink) => sink.write(event, cancel),
#[cfg(feature = "hwencode")]
Self::GpuVideo(_) => unreachable!("handled above"),
}
.map_err(|error| encode_failure("writing media", error))
}
#[cfg(feature = "hwencode")]
pub fn write_gpu_video(
&mut self,
surface: &alpha2_native::EncoderSurface,
cancel: &CancelToken,
) -> Result<(), String> {
let Self::GpuVideo(session) = self else {
return Err("this sink does not take GPU-resident frames".to_owned());
};
if cancel.is_cancelled() {
session.abort();
return Err(encode_error("native encode was cancelled"));
}
session
.encode(surface)
.map_err(|error| encode_failure("writing media", error))
}
pub fn write_prepared_video(
&mut self,
frame: &mut Yuv420pFrameBuffer,
pts: MediaTime,
cancel: &CancelToken,
) -> Result<(), String> {
if cancel.is_cancelled() {
self.abort();
return Err(encode_error("native encode was cancelled"));
}
match self {
Self::AudioVideo(sink) => sink.write_prepared_video(frame, pts),
Self::Video(sink) => sink.write_prepared_video(frame, pts),
#[cfg(feature = "hwencode")]
Self::GpuVideo(_) => {
return Err(encode_error(
"the GPU encode path does not take packed CPU frames",
));
}
}
.map_err(|error| encode_failure("writing media", error))
}
pub fn finish(&mut self, cancel: &CancelToken) -> Result<(), String> {
#[cfg(feature = "hwencode")]
if let Self::GpuVideo(session) = self {
if cancel.is_cancelled() {
session.abort();
return Err(encode_error("native encode was cancelled"));
}
return session
.finish()
.map_err(|error| encode_failure("finalizing output", error));
}
match self {
Self::AudioVideo(sink) => sink.finish(cancel),
Self::Video(sink) => sink.finish(cancel),
#[cfg(feature = "hwencode")]
Self::GpuVideo(_) => unreachable!("handled above"),
}
.map_err(|error| encode_failure("finalizing output", error))
}
pub fn abort(&mut self) {
let result = match self {
Self::AudioVideo(sink) => sink.abort(),
Self::Video(sink) => sink.abort(),
#[cfg(feature = "hwencode")]
Self::GpuVideo(session) => {
session.abort();
Ok(())
}
};
if let Err(error) = result {
alpha2_os::diagnostic!("alpha2 native export cleanup failed: {error}");
}
}
}
pub enum EncodeCommand {
Event {
event: EncodeEvent,
video_index: Option<u64>,
},
PreparedVideo {
frame: PooledFrame,
video_index: u64,
pts: MediaTime,
},
#[cfg(feature = "hwencode")]
GpuVideo {
frame: GpuFrameLease,
video_index: u64,
bytes: u64,
},
Finish,
}
pub enum EncodeWorkerEvent {
FrameReturned(PooledFrame),
MediaComplete {
video_index: Option<u64>,
bytes: u64,
started_at: Duration,
duration: Duration,
sent: Duration,
stages: EncodeStageTimings,
},
Finished(EncodeStageTimings),
Failed(String),
}
fn packed_frame_bytes(frame: &Yuv420pFrameBuffer) -> u64 {
let (width, height) = frame.size();
let luma = u64::try_from(width.saturating_mul(height)).unwrap_or(u64::MAX);
luma.saturating_add(luma / 2)
}
pub fn encode_event_bytes(event: &EncodeEvent) -> u64 {
match event {
EncodeEvent::Video(frame) => frame.planes.iter().fold(0_u64, |bytes, plane| {
bytes.saturating_add(u64::try_from(plane.bytes.len()).unwrap_or(u64::MAX))
}),
EncodeEvent::Audio(block) => audio_block_bytes(block),
}
}
#[derive(Clone, Default)]
pub struct PipelineFailure {
pub cause: Arc<Mutex<Option<String>>>,
}
impl PipelineFailure {
pub fn record(&self, error: &str) {
let mut cause = self
.cause
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if cause.is_none() {
*cause = Some(error.to_owned());
}
}
pub fn record_unless_cancelled(&self, cancel: &CancelToken, error: &str) {
if !cancel.is_cancelled() {
self.record(error);
}
}
pub fn cause_or(&self, fallback: impl Into<String>) -> String {
self.cause
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.unwrap_or_else(|| fallback.into())
}
pub fn report(&self, error: String) -> String {
self.record(&error);
self.cause_or(error)
}
}
pub struct EncodeChannelDepth {
pending: AtomicU64,
histogram: Vec<AtomicU64>,
}
impl EncodeChannelDepth {
pub fn new(depth: usize) -> Arc<Self> {
Arc::new(Self {
pending: AtomicU64::new(0),
histogram: (0..=depth.saturating_add(1))
.map(|_| AtomicU64::new(0))
.collect(),
})
}
pub(crate) fn try_send<T>(
&self,
sender: &SyncSender<T>,
command: T,
) -> Result<(), TrySendError<T>> {
self.pending.fetch_add(1, Ordering::Relaxed);
sender.try_send(command).inspect_err(|_| {
self.pending.fetch_sub(1, Ordering::Relaxed);
})
}
pub(crate) fn taken(&self) {
let previous = self.pending.fetch_sub(1, Ordering::Relaxed);
debug_assert!(
previous > 0,
"the encode worker took a command the channel depth did not count"
);
let remaining = previous.saturating_sub(1);
let slot = usize::try_from(remaining).unwrap_or(usize::MAX);
if let Some(counter) = self.histogram.get(slot) {
counter.fetch_add(1, Ordering::Relaxed);
}
}
pub fn samples(&self) -> Vec<u64> {
self.histogram
.iter()
.map(|counter| counter.load(Ordering::Relaxed))
.collect()
}
}
#[derive(Default)]
pub struct EncodeWorkerIdle(AtomicU64);
impl EncodeWorkerIdle {
fn record(&self, waited: Duration) {
self.0.fetch_add(
u64::try_from(waited.as_nanos()).unwrap_or(u64::MAX),
Ordering::Relaxed,
);
}
pub fn total(&self) -> Duration {
Duration::from_nanos(self.0.load(Ordering::Relaxed))
}
}
pub struct EncodeWorker {
pub commands: Option<SyncSender<EncodeCommand>>,
pub results: Receiver<EncodeWorkerEvent>,
pub join: Option<JoinHandle<()>>,
pub cancel: CancelToken,
pub idle: Arc<EncodeWorkerIdle>,
pub depth: Arc<EncodeChannelDepth>,
pub failure: PipelineFailure,
pub heartbeat: Arc<WorkerHeartbeat>,
pub progress: Arc<CommandProgress>,
pub stall_timeout: Duration,
pub stalled: AtomicBool,
pub finishing: AtomicBool,
}
struct EncodeWorkerState {
sink: NativeExportSink,
commands: Receiver<EncodeCommand>,
results: mpsc::Sender<EncodeWorkerEvent>,
cancel: CancelToken,
failure: PipelineFailure,
idle: Arc<EncodeWorkerIdle>,
depth: Arc<EncodeChannelDepth>,
heartbeat: Arc<WorkerHeartbeat>,
progress: Arc<CommandProgress>,
epoch: Instant,
#[cfg(test)]
fail_video_at: Option<u64>,
#[cfg(test)]
slow_video: Option<Duration>,
}
struct EncodeSpan {
before: EncodeStageTimings,
started: Instant,
started_at: Duration,
bytes: u64,
video_index: Option<u64>,
}
impl EncodeSpan {
fn begin(state: &EncodeWorkerState, bytes: u64, video_index: Option<u64>) -> Self {
let started = Instant::now();
Self {
before: state.sink.stage_timings(),
started_at: started.saturating_duration_since(state.epoch),
started,
bytes,
video_index,
}
}
fn end(self, state: &mut EncodeWorkerState, written: Result<(), String>) -> bool {
#[cfg(test)]
if let (Some(_), Some(slow)) = (self.video_index, state.slow_video) {
thread::sleep(slow.saturating_sub(self.started.elapsed()));
}
#[cfg(test)]
let written = match (written, state.fail_video_at) {
(Ok(()), Some(frame)) if self.video_index == Some(frame) => {
Err(INJECTED_WRITE_FAILURE.to_owned())
}
(written, _) => written,
};
if let Err(error) = written {
let error = crate::failure::tag_phase(
crate::failure::ExportPhase::Encode,
self.video_index,
encode_error(error),
);
state.failure.record_unless_cancelled(&state.cancel, &error);
alpha2_os::diagnostic!(
"alpha2 native encode worker failed: {}",
crate::tag::untagged(&error)
);
let _ = state.results.send(EncodeWorkerEvent::Failed(error));
state.cancel.cancel();
state.sink.abort();
return false;
}
let stages = state.sink.stage_timings();
let call = stages.since(self.before);
let _ = state.results.send(EncodeWorkerEvent::MediaComplete {
video_index: self.video_index,
bytes: self.bytes,
started_at: self.started_at,
duration: self.started.elapsed(),
sent: span_send_time(call),
stages,
});
true
}
}
pub(crate) fn span_send_time(call: EncodeStageTimings) -> Duration {
call.video_send_frame
}
fn run_encode_worker(state: &mut EncodeWorkerState) {
loop {
let idle_started = Instant::now();
let received = state.commands.recv();
state.idle.record(idle_started.elapsed());
let Ok(command) = received else { break };
state.depth.taken();
state.progress.took_one();
match command {
EncodeCommand::Event { event, video_index } => {
let _work = state.heartbeat.begin_work();
let bytes = encode_event_bytes(&event);
let span = EncodeSpan::begin(state, bytes, video_index);
let written = state.sink.write(event, &state.cancel);
if !span.end(state, written) {
return;
}
}
EncodeCommand::PreparedVideo {
mut frame,
video_index,
pts,
} => {
let _work = state.heartbeat.begin_work();
let bytes = packed_frame_bytes(frame.buffer_mut());
let span = EncodeSpan::begin(state, bytes, Some(video_index));
let written =
state
.sink
.write_prepared_video(frame.buffer_mut(), pts, &state.cancel);
let _ = state.results.send(EncodeWorkerEvent::FrameReturned(frame));
if !span.end(state, written) {
return;
}
}
#[cfg(feature = "hwencode")]
EncodeCommand::GpuVideo {
frame,
video_index,
bytes,
} => {
let _work = state.heartbeat.begin_work();
let span = EncodeSpan::begin(state, bytes, Some(video_index));
let written = state.sink.write_gpu_video(frame.surface(), &state.cancel);
drop(frame);
if !span.end(state, written) {
return;
}
}
EncodeCommand::Finish => {
let _work = state.heartbeat.begin_close();
match state.sink.finish(&state.cancel) {
Ok(()) => {
let _ = state
.results
.send(EncodeWorkerEvent::Finished(state.sink.stage_timings()));
}
Err(error) => {
let error = crate::failure::tag_phase(
crate::failure::ExportPhase::Mux,
None,
encode_error(error),
);
state.failure.record_unless_cancelled(&state.cancel, &error);
alpha2_os::diagnostic!(
"alpha2 native encode worker failed: {}",
crate::tag::untagged(&error)
);
let _ = state.results.send(EncodeWorkerEvent::Failed(error));
state.cancel.cancel();
state.sink.abort();
}
}
return;
}
}
}
let _work = state.heartbeat.begin_work();
state.sink.abort();
}
#[cfg(test)]
thread_local! {
pub static REFUSE_ENCODE_THREAD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
pub static INJECT_ENCODE_STALL: std::cell::Cell<Option<(InjectedStall, Duration)>> =
const { std::cell::Cell::new(None) };
pub static INJECT_ENCODE_FAILURE: std::cell::Cell<Option<u64>> =
const { std::cell::Cell::new(None) };
pub static INJECT_SLOW_ENCODE: std::cell::Cell<Option<Duration>> =
const { std::cell::Cell::new(None) };
}
#[cfg(test)]
pub const INJECTED_WRITE_FAILURE: &str =
"native encode failed while writing media: packet write failed: No space left on device";
impl EncodeWorker {
pub fn spawn(
sink: NativeExportSink,
cancel: CancelToken,
failure: PipelineFailure,
channel_depth: usize,
epoch: Instant,
stall_timeout: Duration,
) -> Result<Self, String> {
#[cfg(test)]
let (heartbeat, stall_timeout) = match INJECT_ENCODE_STALL.with(std::cell::Cell::take) {
Some((stall, limit)) => (WorkerHeartbeat::stalling(Some(stall)), limit),
None => (WorkerHeartbeat::default(), stall_timeout),
};
#[cfg(not(test))]
let heartbeat = WorkerHeartbeat::default();
#[cfg(test)]
let fail_video_at = INJECT_ENCODE_FAILURE.with(std::cell::Cell::take);
#[cfg(test)]
let slow_video = INJECT_SLOW_ENCODE.with(std::cell::Cell::take);
let heartbeat = Arc::new(heartbeat);
if sink.is_hardware() {
heartbeat.opened_hardware();
}
let (command_sender, command_receiver) = mpsc::sync_channel(channel_depth);
let (result_sender, result_receiver) = mpsc::channel();
let worker_cancel = cancel.clone();
let worker_failure = failure.clone();
let idle = Arc::new(EncodeWorkerIdle::default());
let worker_idle = Arc::clone(&idle);
let depth = EncodeChannelDepth::new(channel_depth);
let worker_depth = Arc::clone(&depth);
let progress = Arc::new(CommandProgress::default());
let worker_progress = Arc::clone(&progress);
let worker_heartbeat = Arc::clone(&heartbeat);
let run = move || {
let beat = Arc::clone(&worker_heartbeat);
let _watch = alpha2_native::watch_encode_calls(move || beat.beat());
let mut state = EncodeWorkerState {
sink,
commands: command_receiver,
results: result_sender,
cancel: worker_cancel,
failure: worker_failure,
idle: worker_idle,
depth: worker_depth,
heartbeat: Arc::clone(&worker_heartbeat),
progress: worker_progress,
epoch,
#[cfg(test)]
fail_video_at,
#[cfg(test)]
slow_video,
};
run_encode_worker(&mut state);
let _work = worker_heartbeat.begin_work();
drop(state);
};
let builder = thread::Builder::new().name("alpha2-encode".to_owned());
#[cfg(test)]
let spawned = if REFUSE_ENCODE_THREAD.with(std::cell::Cell::take) {
drop(run);
Err(std::io::Error::from(std::io::ErrorKind::WouldBlock))
} else {
builder.spawn(run)
};
#[cfg(not(test))]
let spawned = builder.spawn(run);
let join = spawned.map_err(|error| {
encode_error(format!("failed to start native encode worker: {error}"))
})?;
Ok(Self {
commands: Some(command_sender),
results: result_receiver,
join: Some(join),
cancel,
idle,
depth,
failure,
heartbeat,
progress,
stall_timeout,
stalled: AtomicBool::new(false),
finishing: AtomicBool::new(false),
})
}
pub fn send(&self, command: EncodeCommand) -> Result<bool, String> {
let sender = self
.commands
.as_ref()
.ok_or_else(|| encode_error("encode worker is closed"))?;
if matches!(command, EncodeCommand::Finish) {
self.finishing.store(true, Ordering::Release);
}
let mut pending = command;
let mut waited = false;
loop {
let seen = self.progress.current();
match self.depth.try_send(sender, pending) {
Ok(()) => return Ok(waited),
Err(TrySendError::Full(command)) => pending = command,
Err(TrySendError::Disconnected(_)) => {
return Err(self
.failure
.cause_or(encode_error("encode worker stopped before accepting media")));
}
}
waited = true;
self.progress.wait_past(seen, STALL_POLL);
self.check_stall()?;
}
}
pub fn try_result(&self) -> Result<Option<EncodeWorkerEvent>, String> {
match self.results.try_recv() {
Ok(EncodeWorkerEvent::Failed(error)) => Err(self.failure.cause_or(error)),
Ok(event) => Ok(Some(event)),
Err(TryRecvError::Empty) => self.check_stall().map(|()| None),
Err(TryRecvError::Disconnected) => Err(self.failure.cause_or(encode_error(
"encode worker stopped without a terminal result",
))),
}
}
pub fn result(&self) -> Result<EncodeWorkerEvent, String> {
loop {
match self.results.recv_timeout(STALL_POLL) {
Ok(EncodeWorkerEvent::Failed(error)) => return Err(self.failure.cause_or(error)),
Ok(event) => return Ok(event),
Err(RecvTimeoutError::Timeout) => self.check_stall()?,
Err(RecvTimeoutError::Disconnected) => {
return Err(self.failure.cause_or(encode_error(
"encode worker stopped without a terminal result",
)));
}
}
}
}
pub fn join_success(&mut self) -> Result<(), String> {
drop(self.commands.take());
let Some(join) = self.join.as_ref() else {
return Ok(());
};
while !join.is_finished() {
self.check_stall()?;
thread::sleep(ENCODE_JOIN_POLL);
}
if self.join.take().is_some_and(|join| join.join().is_err()) {
return Err(encode_error("encode worker panicked"));
}
Ok(())
}
pub fn check_stall(&self) -> Result<(), String> {
let Some(stalled_for) = self.heartbeat.stuck_past(self.stall_timeout) else {
return Ok(());
};
self.stalled.store(true, Ordering::Release);
let hardware = self.heartbeat.holds_hardware();
let cause = if hardware {
format!(
"{}: a GPU encoder waiting on work that never completes does this",
crate::protocol::HARDWARE_ENCODE_STALL_MARKER
)
} else {
"in a software encoder: FFmpeg is blocked, most likely writing an output that \
stopped answering"
.to_owned()
};
let phase = if self.finishing.load(Ordering::Acquire) {
crate::failure::ExportPhase::Mux
} else {
crate::failure::ExportPhase::Encode
};
let error = crate::failure::tag_phase(
phase,
None,
crate::failure::hardware_stall(
hardware,
format!(
"{}the encode worker has been inside one encoder call for {} ms, past the {} ms limit, {cause}",
crate::protocol::encode_stalled_tag(),
stalled_for.as_millis(),
self.stall_timeout.as_millis(),
),
),
);
self.failure.record(&error);
self.cancel.cancel();
Err(self.failure.cause_or(error))
}
}
const ENCODE_JOIN_POLL: Duration = Duration::from_millis(1);
impl Drop for EncodeWorker {
fn drop(&mut self) {
let Some(join) = self.join.take() else {
return;
};
self.cancel.cancel();
drop(self.commands.take());
let limit = if self.stalled.load(Ordering::Acquire) {
Duration::ZERO
} else {
self.stall_timeout
};
crate::heartbeat::join_or_abandon(
join,
std::slice::from_ref(&self.heartbeat),
limit,
"encode worker",
);
}
}
#[derive(Default)]
pub struct EncodeProgressState {
pub completed_video: u64,
pub pending_bytes: u64,
pub pending_events: usize,
}
const PROGRESS_FRAME_CADENCE: u64 = 30;
pub fn handle_encode_worker_event(
event: EncodeWorkerEvent,
state: &mut EncodeProgressState,
total: u64,
started: Instant,
progress: &mut dyn FnMut(crate::protocol::Progress) -> Result<(), HostError>,
metrics: &mut ExportMetrics,
) -> Result<bool, String> {
match event {
EncodeWorkerEvent::FrameReturned(frame) => {
drop(frame);
Ok(false)
}
EncodeWorkerEvent::MediaComplete {
video_index,
bytes,
started_at,
duration,
sent,
stages,
} => {
metrics.encode_stages = stages;
if let Some(trace) = metrics.trace.as_mut() {
trace.record_encode(
video_index,
started_at,
started_at.saturating_add(sent),
started_at.saturating_add(duration),
);
}
state.pending_bytes = state.pending_bytes.saturating_sub(bytes);
state.pending_events = state.pending_events.saturating_sub(1);
if let Some(index) = video_index {
metrics.video_encode = metrics.video_encode.saturating_add(duration);
let expected = state.completed_video.saturating_add(1);
if index.saturating_add(1) != expected {
return Err(encode_error(format!(
"encode worker completed video {} while expecting {}",
index.saturating_add(1),
expected
)));
}
state.completed_video = expected;
if state.completed_video.is_multiple_of(PROGRESS_FRAME_CADENCE)
|| state.completed_video == total
{
progress(crate::protocol::Progress::frames(
state.completed_video,
total,
))
.map_err(|error| error.message)?;
let elapsed = started.elapsed().as_secs_f64();
let fps = frames_per_second(state.completed_video, elapsed);
alpha2_os::diagnostic!(
"alpha2 native export: {}/{total} frames ({fps:.1} fps)",
state.completed_video
);
}
} else {
metrics.audio_encode = metrics.audio_encode.saturating_add(duration);
}
Ok(false)
}
EncodeWorkerEvent::Finished(stages) => {
metrics.encode_stages = stages;
Ok(true)
}
EncodeWorkerEvent::Failed(error) => Err(error),
}
}
pub fn drain_encode_worker(
worker: &EncodeWorker,
state: &mut EncodeProgressState,
total: u64,
started: Instant,
progress: &mut dyn FnMut(crate::protocol::Progress) -> Result<(), HostError>,
metrics: &mut ExportMetrics,
) -> Result<(), String> {
while let Some(event) = worker.try_result()? {
if handle_encode_worker_event(event, state, total, started, progress, metrics)? {
return Err(encode_error(
"encode worker finished before the export was closed",
));
}
}
Ok(())
}