every.channel: sanitized baseline

This commit is contained in:
every.channel 2026-02-15 16:17:27 -05:00
commit 897e556bea
No known key found for this signature in database
258 changed files with 74298 additions and 0 deletions

View file

@ -0,0 +1,88 @@
[package]
name = "moq-media"
version = "0.1.0"
edition = "2024"
description = "native audio and video capturing, playback, encoding, decoding"
authors = ["Franz Heinzmann <frando@n0.computer>"]
repository = "https://github.com/n0-computer/iroh-live"
license = "MIT OR Apache-2.0"
[dependencies]
anyhow = "1.0.100"
bytemuck = "1.24.0"
byte-unit = { version = "5.1", features = ["bit"] }
data-encoding = "2.9.0"
derive_more = { version = "2.0.1", features = ["display", "debug", "eq"] }
ffmpeg-next = { version = "8.0.0", default-features = false, features = ["device", "format", "filter", "software-resampling", "software-scaling"] }
ffmpeg-sys-next = { version = "8.0.1", optional = true }
firewheel = { version = "0.9.1", features = ["cpal", "peak_meter_node", "std", "stream_nodes", "cpal_resample_inputs"] }
hang = "0.9.0"
image = { version = "0.25.8", default-features = false }
moq-lite = "0.10.1"
n0-error = { version = "0.1.2", features = ["anyhow"] }
n0-future = "0.3.1"
n0-watcher = "0.6.0"
nokhwa = { version = "0.10", features = [
"input-native",
"input-v4l",
"output-threaded",
] }
postcard = "1.1.3"
rand = "0.9.2"
serde = { version = "1.0.228", features = ["derive"] }
strum = { version = "0.27", features = ["derive"] }
tokio = { version = "1.48.0", features = ["sync"] }
tokio-util = "0.7.17"
tracing = "0.1.41"
xcap = "0.8"
webrtc-audio-processing = { version = "0.5.0", features = ["bundled"] }
bytes = "1.11.0"
buf-list = "1.1.2"
[dev-dependencies]
clap = { version = "4.5", features = ["derive"] }
eframe = "0.33.0"
postcard = "1.1.3"
tokio = { version = "1.48.0", features = ["full"] }
tracing-subscriber = "0.3.20"
[features]
default = []
# Enable static build of ffmpeg
static = [
"ffmpeg-next/static",
"ffmpeg-next/build-lib-openssl",
"ffmpeg-next/build-license-version3",
"ffmpeg-next/build-lib-opus",
"ffmpeg-next/build-lib-x264",
"ffmpeg-next/build-license-gpl",
"dep:ffmpeg-sys-next",
]
[target.'cfg(target_os = "macos")'.dependencies]
ffmpeg-sys-next = { version = "8.0.1", optional = true, features = [
"build-videotoolbox",
"build-audiotoolbox",
] }
[target.'cfg(target_os = "linux")'.dependencies]
ffmpeg-sys-next = { version = "8.0.1", optional = true, features = [
"build-vaapi",
# "build-vulkan",
# "build-lib-libmfx",
] }
[target.'cfg(target_os = "windows")'.dependencies]
ffmpeg-sys-next = { version = "8.0.1", optional = true, features = [
"build-lib-d3d11va",
"build-lib-dxva2",
# "build-nvidia",
# "build-amf",
] }
[target.'cfg(target_os = "android")'.dependencies]
ffmpeg-sys-next = { version = "8.0.1", optional = true, features = [
# "build-mediacodec",
] }

View file

@ -0,0 +1,527 @@
use std::{
collections::HashMap,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
use anyhow::{Context, Result};
use firewheel::{
CpalConfig, CpalInputConfig, CpalOutputConfig, FirewheelConfig, FirewheelContext,
channel_config::{ChannelConfig, ChannelCount, NonZeroChannelCount},
dsp::volume::{DEFAULT_DB_EPSILON, DbMeterNormalizer},
graph::PortIdx,
node::NodeID,
nodes::{
peak_meter::{PeakMeterNode, PeakMeterSmoother, PeakMeterState},
stream::{
ResamplingChannelConfig,
reader::{StreamReaderConfig, StreamReaderNode, StreamReaderState},
writer::{StreamWriterConfig, StreamWriterNode, StreamWriterState},
},
},
};
use tokio::sync::{mpsc, mpsc::error::TryRecvError, oneshot};
use tracing::{debug, error, info, trace, warn};
use self::aec::{AecCaptureNode, AecProcessor, AecProcessorConfig, AecRenderNode};
use crate::{
av::{AudioFormat, AudioSink, AudioSinkHandle, AudioSource},
util::spawn_thread,
};
mod aec;
type StreamWriterHandle = Arc<Mutex<StreamWriterState>>;
type StreamReaderHandle = Arc<Mutex<StreamReaderState>>;
#[derive(Debug, Clone)]
pub struct AudioBackend {
tx: mpsc::Sender<DriverMessage>,
}
impl AudioBackend {
pub fn new() -> Self {
let (tx, rx) = mpsc::channel(32);
let _handle = spawn_thread("audiodriver", move || AudioDriver::new(rx).run());
Self { tx }
}
pub async fn default_input(&self) -> Result<InputStream> {
self.input(AudioFormat::mono_48k()).await
}
pub async fn input(&self, format: AudioFormat) -> Result<InputStream> {
let (reply, reply_rx) = oneshot::channel();
self.tx
.send(DriverMessage::InputStream { format, reply })
.await?;
let handle = reply_rx.await??;
Ok(InputStream { handle, format })
}
pub async fn default_output(&self) -> Result<OutputStream> {
self.output(AudioFormat::stereo_48k()).await
}
pub async fn output(&self, format: AudioFormat) -> Result<OutputStream> {
let (reply, reply_rx) = oneshot::channel();
self.tx
.send(DriverMessage::OutputStream { format, reply })
.await?;
let handle = reply_rx.await??;
Ok(handle)
}
}
#[derive(Clone)]
pub struct OutputStream {
handle: StreamWriterHandle,
paused: Arc<AtomicBool>,
peaks: Arc<Mutex<PeakMeterSmoother<2>>>,
normalizer: DbMeterNormalizer,
}
impl AudioSinkHandle for OutputStream {
fn is_paused(&self) -> bool {
self.paused.load(Ordering::Relaxed)
}
fn pause(&self) {
self.paused.store(true, Ordering::Relaxed);
self.handle.lock().expect("poisoned").pause_stream();
}
fn resume(&self) {
self.paused.store(false, Ordering::Relaxed);
self.handle.lock().expect("poisoned").resume();
}
fn toggle_pause(&self) {
let was_paused = self.paused.fetch_xor(true, Ordering::Relaxed);
if was_paused {
self.handle.lock().expect("poisoned").resume();
} else {
self.handle.lock().expect("poisoned").pause_stream();
}
}
fn smoothed_peak_normalized(&self) -> Option<f32> {
Some(
self.peaks
.lock()
.expect("poisoned")
.smoothed_peaks_normalized_mono(&self.normalizer),
)
}
}
impl AudioSink for OutputStream {
fn handle(&self) -> Box<dyn AudioSinkHandle> {
Box::new(self.clone())
}
fn format(&self) -> Result<AudioFormat> {
let info = self.handle.lock().expect("poisoned");
let sample_rate = info
.sample_rate()
.context("output stream misses sample rate")?
.get();
let channel_count = info.num_channels().get().get();
Ok(AudioFormat {
sample_rate,
channel_count,
})
}
fn push_samples(&mut self, samples: &[f32]) -> Result<()> {
let mut handle = self.handle.lock().unwrap();
// If this happens excessively in Release mode, you may want to consider
// increasing [`StreamWriterConfig::channel_config.latency_seconds`].
if handle.underflow_occurred() {
warn!("Underflow occured in stream writer node!");
}
// If this happens excessively in Release mode, you may want to consider
// increasing [`StreamWriterConfig::channel_config.capacity_seconds`]. For
// example, if you are streaming data from a network, you may want to
// increase the capacity to several seconds.
if handle.overflow_occurred() {
warn!("Overflow occured in stream writer node!");
}
// Wait until the node's processor is ready to receive data.
if handle.is_ready() {
// let expected_bytes =
// frame.samples() * frame.channels() as usize * core::mem::size_of::<f32>();
// let cpal_sample_data: &[f32] = bytemuck::cast_slice(&frame.data(0)[..expected_bytes]);
handle.push_interleaved(samples);
trace!("pushed samples {}", samples.len());
} else {
warn!("output handle is inactive")
}
Ok(())
}
}
impl OutputStream {
#[allow(unused)]
pub fn is_active(&self) -> bool {
self.handle.lock().expect("poisoned").is_active()
}
}
/// A simple AudioSource that reads from the default microphone via Firewheel.
#[derive(Clone)]
pub struct InputStream {
handle: StreamReaderHandle,
format: AudioFormat,
}
impl AudioSource for InputStream {
fn cloned_boxed(&self) -> Box<dyn AudioSource> {
Box::new(self.clone())
}
fn format(&self) -> AudioFormat {
self.format
}
fn pop_samples(&mut self, buf: &mut [f32]) -> Result<Option<usize>> {
use firewheel::nodes::stream::ReadStatus;
let mut handle = self.handle.lock().expect("poisoned");
match handle.read_interleaved(buf) {
Some(ReadStatus::Ok) => Ok(Some(buf.len())),
Some(ReadStatus::InputNotReady) => {
tracing::warn!("audio input not ready");
// Maintain pacing; still return a frame-sized buffer
Ok(Some(buf.len()))
}
Some(ReadStatus::UnderflowOccurred { num_frames_read }) => {
tracing::warn!(
"audio input underflow: {} frames missing",
buf.len() - num_frames_read
);
Ok(Some(buf.len()))
}
Some(ReadStatus::OverflowCorrected {
num_frames_discarded,
}) => {
tracing::warn!("audio input overflow: {num_frames_discarded} frames discarded");
Ok(Some(buf.len()))
}
None => {
tracing::warn!("audio input stream is inactive");
Ok(None)
}
}
}
}
#[derive(derive_more::Debug)]
enum DriverMessage {
OutputStream {
format: AudioFormat,
#[debug("Sender")]
reply: oneshot::Sender<Result<OutputStream>>,
},
InputStream {
format: AudioFormat,
#[debug("Sender")]
reply: oneshot::Sender<Result<StreamReaderHandle>>,
},
}
struct AudioDriver {
cx: FirewheelContext,
rx: mpsc::Receiver<DriverMessage>,
aec_processor: AecProcessor,
aec_render_node: NodeID,
aec_capture_node: NodeID,
peak_meters: HashMap<NodeID, Arc<Mutex<PeakMeterSmoother<2>>>>,
}
impl AudioDriver {
fn new(rx: mpsc::Receiver<DriverMessage>) -> Self {
let config = FirewheelConfig {
num_graph_inputs: ChannelCount::new(1).unwrap(),
..Default::default()
};
let mut cx = FirewheelContext::new(config);
info!("inputs: {:?}", cx.available_input_devices());
info!("outputs: {:?}", cx.available_output_devices());
let config = CpalConfig {
output: CpalOutputConfig {
#[cfg(target_os = "linux")]
device_name: Some("pipewire".to_string()),
..Default::default()
},
input: Some(CpalInputConfig {
#[cfg(target_os = "linux")]
device_name: Some("pipewire".to_string()),
fail_on_no_input: true,
..Default::default()
}),
};
cx.start_stream(config).unwrap();
info!(
"audio graph in: {:?}",
cx.node_info(cx.graph_in_node_id()).map(|x| &x.info)
);
info!(
"audio graph out: {:?}",
cx.node_info(cx.graph_out_node_id()).map(|x| &x.info)
);
cx.set_graph_channel_config(ChannelConfig {
num_inputs: ChannelCount::new(2).unwrap(),
num_outputs: ChannelCount::new(2).unwrap(),
});
let aec_processor = AecProcessor::new(AecProcessorConfig::stereo_in_out(), true)
.expect("failed to initialize AEC processor");
let aec_render_node = cx.add_node(AecRenderNode::default(), Some(aec_processor.clone()));
let aec_capture_node = cx.add_node(AecCaptureNode::default(), Some(aec_processor.clone()));
let layout = &[(0, 0), (1, 1)];
cx.connect(cx.graph_in_node_id(), aec_capture_node, layout, true)
.unwrap();
cx.connect(aec_render_node, cx.graph_out_node_id(), layout, true)
.unwrap();
Self {
cx,
rx,
aec_processor,
aec_render_node,
aec_capture_node,
peak_meters: Default::default(),
}
}
fn run(&mut self) {
const INTERVAL: Duration = Duration::from_millis(10);
const PEAK_UPDATE_INTERVAL: Duration = Duration::from_millis(40);
let mut last_delay: f64 = 0.;
let mut last_peak_update = Instant::now();
loop {
let tick = Instant::now();
if self.drain_messages().is_err() {
info!("closing audio driver: message channel closed");
break;
}
if let Err(e) = self.cx.update() {
error!("audio backend error: {:?}", &e);
// if let UpdateError::StreamStoppedUnexpectedly(_) = e {
// // Notify the stream node handles that the output stream has stopped.
// // This will automatically stop any active streams on the nodes.
// cx.node_state_mut::<StreamWriterState>(stream_writer_id)
// .unwrap()
// .stop_stream();
// cx.node_state_mut::<StreamReaderState>(stream_reader_id)
// .unwrap()
// .stop_stream();
// // The stream has stopped unexpectedly (i.e the user has
// // unplugged their headphones.)
// //
// // Typically you should start a new stream as soon as
// // possible to resume processing (event if it's a dummy
// // output device).
// //
// // In this example we just quit the application.
// break;
// }
}
if let Some(info) = self.cx.stream_info() {
let delay = info.input_to_output_latency_seconds;
if (last_delay - delay).abs() > (1. / 1000.) {
let delay_ms = (delay * 1000.) as u32;
info!("update processor delay to {delay_ms}ms");
self.aec_processor.set_stream_delay(delay_ms);
last_delay = delay;
}
}
// Update peak meters
let delta = last_peak_update.elapsed();
if delta > PEAK_UPDATE_INTERVAL {
for (id, smoother) in self.peak_meters.iter_mut() {
smoother.lock().expect("poisoned").update(
self.cx
.node_state::<PeakMeterState<2>>(*id)
.unwrap()
.peak_gain_db(DEFAULT_DB_EPSILON),
delta.as_secs_f32(),
);
}
last_peak_update = Instant::now();
}
std::thread::sleep(INTERVAL.saturating_sub(tick.elapsed()));
}
}
fn drain_messages(&mut self) -> Result<(), ()> {
loop {
match self.rx.try_recv() {
Err(TryRecvError::Disconnected) => {
info!("stopping audio thread: backend handle dropped");
break Err(());
}
Err(TryRecvError::Empty) => {
break Ok(());
}
Ok(message) => self.handle_message(message),
}
}
}
fn handle_message(&mut self, message: DriverMessage) {
debug!("handle {message:?}");
match message {
DriverMessage::OutputStream { format, reply } => {
let res = self
.output_stream(format)
.inspect_err(|err| warn!("failed to create audio output stream: {err:#}"));
reply.send(res).ok();
}
DriverMessage::InputStream { format, reply } => {
let res = self
.input_stream(format)
.inspect_err(|err| warn!("failed to create audio input stream: {err:#}"));
reply.send(res).ok();
}
}
}
fn output_stream(&mut self, format: AudioFormat) -> Result<OutputStream> {
let channel_count = format.channel_count;
let sample_rate = format.sample_rate;
// setup stream
let stream_writer_id = self.cx.add_node(
StreamWriterNode,
Some(StreamWriterConfig {
channels: NonZeroChannelCount::new(channel_count)
.context("channel count may not be zero")?,
..Default::default()
}),
);
let graph_out = self.aec_render_node;
// let graph_out_info = self
// .cx
// .node_info(graph_out)
// .context("missing audio output node")?;
let peak_meter_node = PeakMeterNode::<2> { enabled: true };
let peak_meter_id = self.cx.add_node(peak_meter_node.clone(), None);
let peak_meter_smoother =
Arc::new(Mutex::new(PeakMeterSmoother::<2>::new(Default::default())));
self.peak_meters
.insert(peak_meter_id, peak_meter_smoother.clone());
self.cx
.connect(peak_meter_id, graph_out, &[(0, 0), (1, 1)], true)
.unwrap();
let layout: &[(PortIdx, PortIdx)] = match channel_count {
0 => anyhow::bail!("audio stream has no channels"),
1 => &[(0, 0), (0, 1)],
_ => &[(0, 0), (1, 1)],
};
self.cx
.connect(stream_writer_id, peak_meter_id, layout, false)
.unwrap();
let output_stream_sample_rate = self.cx.stream_info().unwrap().sample_rate;
let event = self
.cx
.node_state_mut::<StreamWriterState>(stream_writer_id)
.unwrap()
.start_stream(
sample_rate.try_into().unwrap(),
output_stream_sample_rate,
ResamplingChannelConfig {
capacity_seconds: 3.,
..Default::default()
},
)
.unwrap();
info!("started output stream");
self.cx.queue_event_for(stream_writer_id, event.into());
// Wrap the handles in an `Arc<Mutex<T>>>` so that we can send them to other threads.
let handle = self
.cx
.node_state::<StreamWriterState>(stream_writer_id)
.unwrap()
.handle();
Ok(OutputStream {
handle: Arc::new(handle),
paused: Arc::new(AtomicBool::new(false)),
peaks: peak_meter_smoother,
normalizer: DbMeterNormalizer::new(-60., 0., -20.),
})
}
fn input_stream(&mut self, format: AudioFormat) -> Result<StreamReaderHandle> {
let sample_rate = format.sample_rate;
let channel_count = format.channel_count;
// Setup stream reader node
let stream_reader_id = self.cx.add_node(
StreamReaderNode,
Some(StreamReaderConfig {
channels: NonZeroChannelCount::new(channel_count)
.context("channel count may not be zero")?,
..Default::default()
}),
);
let graph_in_node_id = self.aec_capture_node;
let graph_in_info = self
.cx
.node_info(graph_in_node_id)
.context("missing audio input node")?;
let layout: &[(PortIdx, PortIdx)] = match (
graph_in_info.info.channel_config.num_outputs.get(),
channel_count,
) {
(0, _) => anyhow::bail!("audio input has no channels"),
(1, 2) => &[(0, 0), (0, 1)],
(2, 2) => &[(0, 0), (1, 1)],
(_, 1) => &[(0, 0)],
_ => &[(0, 0), (1, 1)],
};
self.cx
.connect(graph_in_node_id, stream_reader_id, layout, false)
.unwrap();
let input_stream_sample_rate = self.cx.stream_info().unwrap().sample_rate;
let event = self
.cx
.node_state_mut::<StreamReaderState>(stream_reader_id)
.unwrap()
.start_stream(
sample_rate.try_into().unwrap(),
input_stream_sample_rate,
ResamplingChannelConfig {
capacity_seconds: 3.0,
..Default::default()
},
)
.unwrap();
self.cx.queue_event_for(stream_reader_id, event.into());
let handle = self
.cx
.node_state::<StreamReaderState>(stream_reader_id)
.unwrap()
.handle();
Ok(Arc::new(handle))
}
}

View file

@ -0,0 +1,452 @@
pub use self::{
firewheel_nodes::{AecCaptureNode, AecRenderNode},
processor::{AecProcessor, AecProcessorConfig},
};
mod processor {
use std::{
num::NonZeroU32,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
};
use anyhow::Result;
use tracing::{debug, info};
use webrtc_audio_processing::{
Config, EchoCancellation, EchoCancellationSuppressionLevel, InitializationConfig,
};
#[derive(Debug, Clone)]
pub struct AecProcessorConfig {
pub num_input_channels: NonZeroU32,
pub num_output_channels: NonZeroU32,
}
impl Default for AecProcessorConfig {
fn default() -> Self {
Self {
num_input_channels: 2.try_into().unwrap(),
num_output_channels: 2.try_into().unwrap(),
}
}
}
impl AecProcessorConfig {
pub fn stereo_in_out() -> Self {
Self::default()
}
}
#[derive(Clone, Debug)]
pub struct AecProcessor(Arc<Inner>);
#[derive(derive_more::Debug)]
struct Inner {
#[debug("Processor")]
processor: Mutex<webrtc_audio_processing::Processor>,
config: Mutex<Config>,
// capture_delay: AtomicU64,
// playback_delay: AtomicU64,
enabled: AtomicBool,
// capture_channels: AtomicUsize,
// playback_channels: AtomicUsize,
}
impl Default for AecProcessor {
fn default() -> Self {
Self::new(Default::default(), true).expect("failed to initialize AecProcessor")
}
}
impl AecProcessor {
pub fn new(config: AecProcessorConfig, enabled: bool) -> anyhow::Result<Self> {
let suppression_level = EchoCancellationSuppressionLevel::High;
// High pass filter is a prerequisite to running echo cancellation.
let processor_config = Config {
echo_cancellation: Some(EchoCancellation {
suppression_level,
stream_delay_ms: None,
enable_delay_agnostic: true,
enable_extended_filter: true,
}),
enable_high_pass_filter: true,
..Config::default()
};
let mut processor = webrtc_audio_processing::Processor::new(&InitializationConfig {
num_capture_channels: config.num_input_channels.get() as i32,
num_render_channels: config.num_output_channels.get() as i32,
enable_experimental_agc: true,
enable_intelligibility_enhancer: true, // ..InitializationConfig::default()
})?;
processor.set_config(processor_config.clone());
// processor.set_config(config.clone());
info!("init audio processor (config={config:?})");
Ok(Self(Arc::new(Inner {
processor: Mutex::new(processor),
config: Mutex::new(processor_config),
enabled: AtomicBool::new(enabled),
})))
}
pub fn is_enabled(&self) -> bool {
self.0.enabled.load(Ordering::SeqCst)
}
#[allow(unused)]
pub fn set_enabled(&self, enabled: bool) {
let _prev = self.0.enabled.swap(enabled, Ordering::SeqCst);
}
/// Processes and modifies the audio frame from a capture device by applying
/// signal processing as specified in the config. `frame` should hold an
/// interleaved f32 audio frame, with [`NUM_SAMPLES_PER_FRAME`] samples.
// webrtc-audio-processing expects a 10ms chunk for each process call.
pub fn process_capture_frame(
&self,
frame: &mut [f32],
) -> Result<(), webrtc_audio_processing::Error> {
if !self.is_enabled() {
return Ok(());
}
self.0
.processor
.lock()
.expect("poisoned")
.process_capture_frame(frame)
}
/// Processes and optionally modifies the audio frame from a playback device.
/// `frame` should hold an interleaved `f32` audio frame, with
/// [`NUM_SAMPLES_PER_FRAME`] samples.
pub fn process_render_frame(
&self,
frame: &mut [f32],
) -> Result<(), webrtc_audio_processing::Error> {
if !self.is_enabled() {
return Ok(());
}
self.0
.processor
.lock()
.expect("poisoned")
.process_render_frame(frame)
}
pub fn set_stream_delay(&self, delay_ms: u32) {
debug!("updating stream delay to {delay_ms}ms");
// let playback = self.0.playback_delay.load(Ordering::Relaxed);
// let capture = self.0.capture_delay.load(Ordering::Relaxed);
// let total = playback + capture;
let mut config = self.0.config.lock().expect("poisoned");
config.echo_cancellation.as_mut().unwrap().stream_delay_ms = Some(delay_ms as i32);
self.0
.processor
.lock()
.expect("poisoned")
.set_config(config.clone());
}
}
}
mod firewheel_nodes {
use std::collections::VecDeque;
use firewheel::{
StreamInfo,
channel_config::{ChannelConfig, ChannelCount},
diff::{Diff, Patch},
event::ProcEvents,
node::{
AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, ProcBuffers,
ProcExtra, ProcInfo, ProcStreamCtx, ProcessStatus,
},
};
use webrtc_audio_processing::NUM_SAMPLES_PER_FRAME;
use super::AecProcessor;
const CHANNELS: usize = 2;
const FRAME_SAMPLES: usize = (NUM_SAMPLES_PER_FRAME as usize) * CHANNELS;
/// Simple render-side node: feeds output audio into WebRTC's render stream.
#[derive(Diff, Patch, Debug, Clone, Copy, PartialEq)]
pub struct AecRenderNode {
pub enabled: bool,
}
impl Default for AecRenderNode {
fn default() -> Self {
Self { enabled: true }
}
}
impl AudioNode for AecRenderNode {
/// We use the wrapped WebRTC processor as our configuration object.
///
/// Note: `WebrtcAudioProcessor` already internally wraps an `Arc<Inner>`,
/// so cloning this config shares the underlying processor between nodes.
type Configuration = AecProcessor;
fn info(&self, _config: &Self::Configuration) -> AudioNodeInfo {
AudioNodeInfo::new()
.debug_name("webrtc_render")
.channel_config(ChannelConfig {
num_inputs: ChannelCount::STEREO,
num_outputs: ChannelCount::STEREO,
})
}
fn construct_processor(
&self,
config: &Self::Configuration,
_cx: ConstructProcessorContext,
) -> impl AudioNodeProcessor {
// Clone = share the same underlying Arc<Inner>.
let webrtc = config.clone();
// Inform the processor how many playback channels we have.
// (You can handle errors here instead of unwrap() in real code.)
// webrtc.init_playback(CHANNELS).ok();
RenderProcessor {
enabled: self.enabled,
processor: webrtc,
in_ring: VecDeque::with_capacity(FRAME_SAMPLES * 4),
out_ring: VecDeque::with_capacity(FRAME_SAMPLES * 4),
tmp_chunk: vec![0.0; FRAME_SAMPLES],
}
}
}
struct RenderProcessor {
enabled: bool,
processor: AecProcessor,
// Interleaved input samples to be fed into WebRTC in 10ms chunks.
in_ring: VecDeque<f32>,
// Interleaved processed samples coming back from WebRTC.
out_ring: VecDeque<f32>,
// Scratch buffer for one NUM_SAMPLES_PER_FRAME chunk (interleaved).
tmp_chunk: Vec<f32>,
}
impl AudioNodeProcessor for RenderProcessor {
fn process(
&mut self,
info: &ProcInfo,
buffers: ProcBuffers,
events: &mut ProcEvents,
_extra: &mut ProcExtra,
) -> ProcessStatus {
// Handle parameter patches.
for patch in events.drain_patches::<AecRenderNode>() {
match patch {
AecRenderNodePatch::Enabled(enabled) => {
self.enabled = enabled;
if !self.enabled {
// Clear any buffered state when disabling to avoid stale audio.
self.in_ring.clear();
self.out_ring.clear();
}
}
}
}
let num_frames = info.frames as usize;
// println!("num_frames: {num_frames}");
// Get input/output slices like in the FilterNode example.
let in_l = &buffers.inputs[0][..num_frames];
let in_r = &buffers.inputs[1][..num_frames];
let (out_l, out_rest) = buffers.outputs.split_first_mut().unwrap();
let out_l = &mut out_l[..num_frames];
let out_r = &mut out_rest[0][..num_frames];
// If disabled, just pass through.
if !self.enabled {
out_l.copy_from_slice(in_l);
out_r.copy_from_slice(in_r);
return ProcessStatus::OutputsModified;
}
// 1. Push current block into the interleaved input ring buffer.
for i in 0..num_frames {
self.in_ring.push_back(in_l[i]);
self.in_ring.push_back(in_r[i]);
}
// 2. While we have at least one full 10ms frame, process it.
while self.in_ring.len() >= FRAME_SAMPLES {
// Fill tmp_chunk with a full frame of interleaved samples.
for s in &mut self.tmp_chunk[..FRAME_SAMPLES] {
*s = self.in_ring.pop_front().unwrap();
}
// Feed into processor render stream.
let _ = self.processor.process_render_frame(&mut self.tmp_chunk);
// Store processed samples into the output ring.
for &s in &self.tmp_chunk[..FRAME_SAMPLES] {
self.out_ring.push_back(s);
}
}
// 3. Produce outputs for this audio block.
//
// We always need `num_frames * CHANNELS` samples. If we don't have
// enough processed samples yet, we output silence for the missing part.
for i in 0..num_frames {
if self.out_ring.len() >= CHANNELS {
out_l[i] = self.out_ring.pop_front().unwrap();
out_r[i] = self.out_ring.pop_front().unwrap();
} else {
// Not enough processed data yet -> output silence.
out_l[i] = 0.0;
out_r[i] = 0.0;
}
}
ProcessStatus::OutputsModified
}
fn new_stream(&mut self, _stream_info: &StreamInfo, _ctx: &mut ProcStreamCtx) {
// Reset buffers for new stream.
self.in_ring.clear();
self.out_ring.clear();
}
}
/// Capture-side node: feeds mic audio into [`AecProcessor`]'s capture stream.
#[derive(Diff, Patch, Debug, Clone, Copy, PartialEq)]
pub struct AecCaptureNode {
pub enabled: bool,
}
impl Default for AecCaptureNode {
fn default() -> Self {
Self { enabled: true }
}
}
impl AudioNode for AecCaptureNode {
type Configuration = AecProcessor;
fn info(&self, _config: &Self::Configuration) -> AudioNodeInfo {
AudioNodeInfo::new()
.debug_name("webrtc_capture")
.channel_config(ChannelConfig {
num_inputs: ChannelCount::STEREO,
num_outputs: ChannelCount::STEREO,
})
}
fn construct_processor(
&self,
config: &Self::Configuration,
_cx: ConstructProcessorContext,
) -> impl AudioNodeProcessor {
CaptureProcessor {
enabled: self.enabled,
processor: config.clone(),
in_ring: VecDeque::with_capacity(FRAME_SAMPLES * 4),
out_ring: VecDeque::with_capacity(FRAME_SAMPLES * 4),
tmp_chunk: vec![0.0; FRAME_SAMPLES],
}
}
}
struct CaptureProcessor {
enabled: bool,
processor: AecProcessor,
// Interleaved input samples to be fed into WebRTC in 10ms chunks.
in_ring: VecDeque<f32>,
// Interleaved processed samples coming back from WebRTC.
out_ring: VecDeque<f32>,
// Scratch buffer for one NUM_SAMPLES_PER_FRAME chunk (interleaved).
tmp_chunk: Vec<f32>,
}
impl AudioNodeProcessor for CaptureProcessor {
fn process(
&mut self,
info: &ProcInfo,
buffers: ProcBuffers,
events: &mut ProcEvents,
_extra: &mut ProcExtra,
) -> ProcessStatus {
for patch in events.drain_patches::<AecCaptureNode>() {
match patch {
AecCaptureNodePatch::Enabled(enabled) => {
self.enabled = enabled;
if !self.enabled {
self.in_ring.clear();
self.out_ring.clear();
}
}
}
}
let frames = info.frames;
let num_frames = frames as usize;
let in_l = &buffers.inputs[0][..num_frames];
let in_r = &buffers.inputs[1][..num_frames];
let (out_l, out_rest) = buffers.outputs.split_first_mut().unwrap();
let out_l = &mut out_l[..num_frames];
let out_r = &mut out_rest[0][..num_frames];
if !self.enabled {
// Bypass if disabled.
out_l.copy_from_slice(in_l);
out_r.copy_from_slice(in_r);
return ProcessStatus::OutputsModified;
}
// 1. Push current block into the interleaved input ring buffer.
for i in 0..num_frames {
self.in_ring.push_back(in_l[i]);
self.in_ring.push_back(in_r[i]);
}
// 2. While we have at least one full 10ms frame, process it.
while self.in_ring.len() >= FRAME_SAMPLES {
for s in &mut self.tmp_chunk[..FRAME_SAMPLES] {
*s = self.in_ring.pop_front().unwrap();
}
let _ = self.processor.process_capture_frame(&mut self.tmp_chunk);
for &s in &self.tmp_chunk[..FRAME_SAMPLES] {
self.out_ring.push_back(s);
}
}
// 3. Produce outputs for this audio block.
//
// If we don't have enough processed samples to cover the whole block,
// we output silence for the missing frames.
for i in 0..num_frames {
if self.out_ring.len() >= CHANNELS {
out_l[i] = self.out_ring.pop_front().unwrap();
out_r[i] = self.out_ring.pop_front().unwrap();
} else {
out_l[i] = 0.0;
out_r[i] = 0.0;
}
}
ProcessStatus::OutputsModified
}
fn new_stream(&mut self, _stream_info: &StreamInfo, _ctx: &mut ProcStreamCtx) {
// Reset state for new stream.
self.in_ring.clear();
self.out_ring.clear();
}
}
}

View file

@ -0,0 +1,265 @@
use std::time::Duration;
use anyhow::Result;
use image::RgbaImage;
use strum::{Display, EnumString, VariantNames};
#[derive(Copy, Clone, Debug)]
pub struct AudioFormat {
pub sample_rate: u32,
pub channel_count: u32,
}
impl AudioFormat {
pub fn mono_48k() -> Self {
Self {
sample_rate: 48_000,
channel_count: 1,
}
}
pub fn stereo_48k() -> Self {
Self {
sample_rate: 48_000,
channel_count: 2,
}
}
pub fn from_hang_config(config: &hang::catalog::AudioConfig) -> Self {
Self {
channel_count: config.channel_count,
sample_rate: config.sample_rate,
}
}
}
pub trait Decoders {
type Audio: AudioDecoder;
type Video: VideoDecoder;
}
pub trait AudioSource: Send + 'static {
fn cloned_boxed(&self) -> Box<dyn AudioSource>;
fn format(&self) -> AudioFormat;
fn pop_samples(&mut self, buf: &mut [f32]) -> Result<Option<usize>>;
}
pub trait AudioSink: AudioSinkHandle {
fn format(&self) -> Result<AudioFormat>;
fn push_samples(&mut self, buf: &[f32]) -> Result<()>;
fn handle(&self) -> Box<dyn AudioSinkHandle>;
}
pub trait AudioSinkHandle: Send + 'static {
fn pause(&self);
fn resume(&self);
fn is_paused(&self) -> bool;
fn toggle_pause(&self);
/// Smoothed peak, normalized to 0..1.
// TODO: document how smoothing and normalization are expected
fn smoothed_peak_normalized(&self) -> Option<f32> {
None
}
}
pub trait AudioEncoder: AudioEncoderInner {
fn with_preset(format: AudioFormat, preset: AudioPreset) -> Result<Self>
where
Self: Sized;
}
pub trait AudioEncoderInner: Send + 'static {
fn name(&self) -> &str;
fn config(&self) -> hang::catalog::AudioConfig;
fn push_samples(&mut self, samples: &[f32]) -> Result<()>;
fn pop_packet(&mut self) -> Result<Option<hang::Frame>>;
}
impl AudioEncoderInner for Box<dyn AudioEncoder> {
fn name(&self) -> &str {
(&**self).name()
}
fn config(&self) -> hang::catalog::AudioConfig {
(&**self).config()
}
fn push_samples(&mut self, samples: &[f32]) -> Result<()> {
(&mut **self).push_samples(samples)
}
fn pop_packet(&mut self) -> Result<Option<hang::Frame>> {
(&mut **self).pop_packet()
}
}
pub trait AudioDecoder: Send + 'static {
fn new(config: &hang::catalog::AudioConfig, target_format: AudioFormat) -> Result<Self>
where
Self: Sized;
fn push_packet(&mut self, packet: hang::Frame) -> Result<()>;
fn pop_samples(&mut self) -> Result<Option<&[f32]>>;
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PixelFormat {
Rgba,
Bgra,
}
impl Default for PixelFormat {
fn default() -> Self {
PixelFormat::Rgba
}
}
#[derive(Clone, Debug)]
pub struct VideoFormat {
pub pixel_format: PixelFormat,
pub dimensions: [u32; 2],
}
#[derive(Clone, Debug)]
pub struct VideoFrame {
pub format: VideoFormat,
pub raw: bytes::Bytes,
}
pub trait VideoSource: Send + 'static {
fn name(&self) -> &str;
fn format(&self) -> VideoFormat;
fn pop_frame(&mut self) -> Result<Option<VideoFrame>>;
fn start(&mut self) -> Result<()>;
fn stop(&mut self) -> Result<()>;
}
pub trait VideoEncoder: VideoEncoderInner {
fn with_preset(preset: VideoPreset) -> Result<Self>
where
Self: Sized;
}
pub trait VideoEncoderInner: Send + 'static {
fn name(&self) -> &str;
fn config(&self) -> hang::catalog::VideoConfig;
fn push_frame(&mut self, frame: VideoFrame) -> Result<()>;
fn pop_packet(&mut self) -> Result<Option<hang::Frame>>;
}
impl VideoEncoderInner for Box<dyn VideoEncoder> {
fn name(&self) -> &str {
(&**self).name()
}
fn config(&self) -> hang::catalog::VideoConfig {
(&**self).config()
}
fn push_frame(&mut self, frame: VideoFrame) -> Result<()> {
(&mut **self).push_frame(frame)
}
fn pop_packet(&mut self) -> Result<Option<hang::Frame>> {
(&mut **self).pop_packet()
}
}
pub trait VideoDecoder: Send + 'static {
fn new(config: &hang::catalog::VideoConfig, playback_config: &DecodeConfig) -> Result<Self>
where
Self: Sized;
fn name(&self) -> &str;
fn pop_frame(&mut self) -> Result<Option<DecodedFrame>>;
fn push_packet(&mut self, packet: hang::Frame) -> Result<()>;
fn set_viewport(&mut self, w: u32, h: u32);
}
pub struct DecodedFrame {
pub frame: image::Frame,
pub timestamp: Duration,
}
impl DecodedFrame {
pub fn img(&self) -> &RgbaImage {
self.frame.buffer()
}
}
#[derive(Debug, Clone, Copy, Display, EnumString, VariantNames)]
#[strum(serialize_all = "lowercase")]
pub enum AudioCodec {
Opus,
}
#[derive(Debug, Clone, Copy, Display, EnumString, VariantNames)]
#[strum(serialize_all = "lowercase")]
pub enum VideoCodec {
H264,
Av1,
}
#[derive(Debug, Clone, Copy, Display, EnumString, VariantNames, Eq, PartialEq, Ord, PartialOrd)]
pub enum VideoPreset {
#[strum(serialize = "180p")]
P180,
#[strum(serialize = "360p")]
P360,
#[strum(serialize = "720p")]
P720,
#[strum(serialize = "1080p")]
P1080,
}
impl VideoPreset {
pub fn all() -> [VideoPreset; 4] {
[Self::P180, Self::P360, Self::P720, Self::P1080]
}
pub fn dimensions(&self) -> (u32, u32) {
match self {
Self::P180 => (320, 180),
Self::P360 => (640, 360),
Self::P720 => (1280, 720),
Self::P1080 => (1920, 1080),
}
}
pub fn width(&self) -> u32 {
self.dimensions().0
}
pub fn height(&self) -> u32 {
self.dimensions().1
}
pub fn fps(&self) -> u32 {
30
}
}
#[derive(Debug, Clone, Copy, Display, EnumString, VariantNames, Eq, PartialEq)]
#[strum(serialize_all = "lowercase")]
pub enum AudioPreset {
Hq,
Lq,
}
#[derive(Debug, Clone, Copy, Display, EnumString, VariantNames, Eq, PartialEq, Default)]
#[strum(serialize_all = "lowercase")]
pub enum Quality {
Highest,
#[default]
High,
Mid,
Low,
}
#[derive(Clone, Default)]
pub struct DecodeConfig {
pub pixel_format: PixelFormat,
}
#[derive(Clone, Default)]
pub struct PlaybackConfig {
pub decode_config: DecodeConfig,
pub quality: Quality,
}

View file

@ -0,0 +1,233 @@
use std::str::FromStr;
use anyhow::{Context, Result};
use nokhwa::{
nokhwa_initialize,
pixel_format::RgbFormat,
utils::{
CameraFormat, CameraIndex, FrameFormat, RequestedFormat, RequestedFormatType, Resolution,
},
};
use tracing::{debug, info, trace, warn};
use xcap::{Monitor, VideoRecorder};
use crate::{
av::{PixelFormat, VideoFormat, VideoFrame, VideoSource},
ffmpeg::util::MjpgDecoder,
};
pub struct ScreenCapturer {
pub(crate) _monitor: Monitor,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) video_recorder: VideoRecorder,
pub(crate) rx: std::sync::mpsc::Receiver<xcap::Frame>,
}
// TODO: Review if sound.
unsafe impl Send for ScreenCapturer {}
impl Drop for ScreenCapturer {
fn drop(&mut self) {
self.video_recorder.stop().ok();
}
}
impl ScreenCapturer {
pub fn new() -> Result<Self> {
info!("Initializing screen capturer (xcap)");
let monitors = Monitor::all().context("Failed to get monitors")?;
if monitors.is_empty() {
return Err(anyhow::anyhow!("No monitors available"));
}
info!("Available monitors: {monitors:?}");
let monitor = monitors.into_iter().next().unwrap();
let width = monitor.width()?;
let height = monitor.height()?;
let name = monitor
.name()
.unwrap_or_else(|_| "Unknown Monitor".to_string());
info!("Using monitor: {} ({}x{})", name, width, height);
let (video_recorder, rx) = monitor.video_recorder()?;
Ok(Self {
_monitor: monitor,
video_recorder,
rx,
width,
height,
})
}
}
impl VideoSource for ScreenCapturer {
fn name(&self) -> &str {
"screen"
}
fn format(&self) -> VideoFormat {
VideoFormat {
pixel_format: PixelFormat::Rgba,
dimensions: [self.width, self.height],
}
}
fn start(&mut self) -> Result<()> {
self.video_recorder.start()?;
Ok(())
}
fn stop(&mut self) -> Result<()> {
self.video_recorder.stop()?;
Ok(())
}
fn pop_frame(&mut self) -> anyhow::Result<Option<VideoFrame>> {
let mut raw_frame = None;
// We are only interested in the latest frame.
// Drain the channel to not build up memory.
while let Ok(next) = self.rx.try_recv() {
raw_frame = Some(next)
}
let raw_frame = match raw_frame {
Some(frame) => frame,
None => self
.rx
.recv()
.context("Screen recorder did not produce new frame")?,
};
Ok(Some(VideoFrame {
format: VideoFormat {
pixel_format: PixelFormat::Rgba,
dimensions: [raw_frame.width, raw_frame.height],
},
raw: raw_frame.raw.into(),
}))
}
}
pub struct CameraCapturer {
pub(crate) camera: nokhwa::Camera,
pub(crate) mjpg_decoder: MjpgDecoder,
pub(crate) width: u32,
pub(crate) height: u32,
}
impl CameraCapturer {
pub fn new() -> Result<Self> {
info!("Initializing camera capturer (nokhwa)");
nokhwa_initialize(|granted| {
debug!("User selected camera access: {}", granted);
});
let cameras = nokhwa::query(nokhwa::utils::ApiBackend::Auto)?;
if cameras.is_empty() {
return Err(anyhow::anyhow!("No cameras available"));
}
info!("Available cameras: {cameras:?}");
let camera_index = match std::env::var("IROH_LIVE_CAMERA").ok() {
None => {
// Order of cameras in nokhwa is reversed from usual order (primary camera is last).
let first_camera = cameras.last().unwrap();
info!(": {}", first_camera.human_name());
first_camera.index().clone()
}
Some(camera_name) => match u32::from_str(&camera_name).ok() {
Some(num) => CameraIndex::Index(num),
None => CameraIndex::String(camera_name),
},
};
let mut camera = nokhwa::Camera::new(
camera_index,
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestResolution),
)?;
info!("Using camera: {}", camera.info().human_name());
let available_formats = camera.compatible_camera_formats()?;
debug!("Available formats: {available_formats:?}",);
if let Some(format) = Self::select_format(available_formats, Resolution::new(1920, 1080)) {
if let Err(err) = camera.set_camera_requset(RequestedFormat::new::<RgbFormat>(
RequestedFormatType::Exact(format),
)) {
warn!(?format, "Failed to change camera format: {err:#}");
}
}
info!("Using format: {}", camera.camera_format());
let resolution = camera.resolution();
Ok(Self {
camera,
mjpg_decoder: MjpgDecoder::new()?,
width: resolution.width(),
height: resolution.height(),
})
}
fn select_format(
mut formats: Vec<CameraFormat>,
desired_resolution: Resolution,
) -> Option<CameraFormat> {
formats.sort_by(|a, b| {
a.resolution()
.cmp(&b.resolution())
.then(a.frame_rate().cmp(&b.frame_rate()))
});
formats
.iter()
.find(|format| format.resolution() >= desired_resolution)
.or_else(|| formats.last())
.cloned()
}
}
impl VideoSource for CameraCapturer {
fn name(&self) -> &str {
"cam"
}
fn format(&self) -> VideoFormat {
VideoFormat {
pixel_format: PixelFormat::Rgba,
dimensions: [self.width, self.height],
}
}
fn start(&mut self) -> Result<()> {
self.camera.open_stream()?;
Ok(())
}
fn stop(&mut self) -> Result<()> {
self.camera.stop_stream()?;
Ok(())
}
fn pop_frame(&mut self) -> anyhow::Result<Option<VideoFrame>> {
let start = std::time::Instant::now();
let frame = self
.camera
.frame()
.context("Failed to capture camera frame")?;
trace!("pop frame: capture took {:?}", start.elapsed());
let start = std::time::Instant::now();
let frame = match frame.source_frame_format() {
FrameFormat::MJPEG if std::env::var("IROH_LIVE_MJPEG_FFMPEG").is_ok() => {
trace!("decode ffmpeg");
self.mjpg_decoder.decode_frame(frame.buffer())?
}
_ => {
let image = frame
.decode_image::<nokhwa::pixel_format::RgbAFormat>()
.context("Failed to decode camera frame")?;
VideoFrame {
format: self.format(),
raw: image.into_raw().into(),
}
}
};
trace!("pop frame: decode took {:?}", start.elapsed());
Ok(Some(frame))
}
}

View file

@ -0,0 +1,93 @@
use anyhow::Result;
use ffmpeg_next::{self as ffmpeg, util::channel_layout::ChannelLayout};
use hang::catalog::AudioConfig;
use crate::{
av::{AudioDecoder, AudioFormat},
ffmpeg::ext::{CodecContextExt, PacketExt},
};
pub struct FfmpegAudioDecoder {
codec: ffmpeg::decoder::Audio,
resampler: ffmpeg::software::resampling::Context,
decoded_frame: ffmpeg::util::frame::Audio,
resampled_frame: ffmpeg::util::frame::Audio,
}
impl AudioDecoder for FfmpegAudioDecoder {
fn new(config: &AudioConfig, target_format: AudioFormat) -> Result<Self>
where
Self: Sized,
{
let codec = match config.codec {
hang::catalog::AudioCodec::Opus => {
let codec_id = ffmpeg::codec::Id::OPUS;
let codec = ffmpeg::decoder::find(codec_id).unwrap();
let mut ctx = ffmpeg::codec::Context::new_with_codec(codec)
.decoder()
.audio()?;
if let Some(extradata) = &config.description {
ctx.set_extradata(&extradata)?;
}
ctx.set_channel_layout(if config.channel_count == 1 {
ChannelLayout::MONO
} else {
ChannelLayout::STEREO
});
unsafe {
let ctx_mut = ctx.as_mut_ptr();
(*ctx_mut).sample_rate = config.sample_rate as i32;
}
ctx
}
_ => anyhow::bail!(
"Unsupported codec {} (only opus is supported)",
config.codec
),
};
let target_channel_layout = match target_format.channel_count {
1 => ChannelLayout::MONO,
2 => ChannelLayout::STEREO,
_ => anyhow::bail!("unsupported target channel count"),
};
let target_sample_format = ffmpeg_next::util::format::sample::Sample::F32(
ffmpeg_next::util::format::sample::Type::Packed,
);
let resampler = ffmpeg::software::resampling::Context::get(
codec.format(),
codec.channel_layout(),
codec.rate(),
target_sample_format,
target_channel_layout,
target_format.sample_rate,
)?;
Ok(Self {
codec,
resampler,
decoded_frame: ffmpeg::util::frame::Audio::empty(),
resampled_frame: ffmpeg::util::frame::Audio::empty(),
})
}
fn push_packet(&mut self, packet: hang::Frame) -> Result<()> {
let packet = packet.payload.to_ffmpeg_packet();
self.codec.send_packet(&packet)?;
Ok(())
}
fn pop_samples(&mut self) -> Result<Option<&[f32]>> {
match self.codec.receive_frame(&mut self.decoded_frame) {
Err(err) => Err(err.into()),
Ok(()) => {
// Create an empty frame to hold the resampled audio data.
self.resampler
.run(&self.decoded_frame, &mut self.resampled_frame)
.unwrap();
let frame = &self.resampled_frame;
let expected_bytes =
frame.samples() * frame.channels() as usize * core::mem::size_of::<f32>();
Ok(Some(bytemuck::cast_slice(&frame.data(0)[..expected_bytes])))
}
}
}
}

View file

@ -0,0 +1,153 @@
use anyhow::{Context, Result};
use ffmpeg_next::{self as ffmpeg, Rational};
use hang::{Timestamp, catalog::AudioConfig};
use tracing::trace;
use crate::{
av::{AudioEncoder, AudioEncoderInner, AudioFormat, AudioPreset},
ffmpeg::ext::CodecContextExt,
};
const SAMPLE_RATE: u32 = 48_000;
const BITRATE: u64 = 128_000; // 128 kbps
pub struct OpusEncoder {
encoder: ffmpeg::encoder::Audio,
frame_count: u64,
sample_rate: u32,
bitrate: u64,
channel_count: u32,
extradata: Vec<u8>,
}
impl OpusEncoder {
pub fn stereo() -> Result<Self> {
Self::new(SAMPLE_RATE, 2, BITRATE)
}
pub fn mono() -> Result<Self> {
Self::new(SAMPLE_RATE, 1, BITRATE)
}
pub fn new(sample_rate: u32, channel_count: u32, bitrate: u64) -> Result<Self> {
tracing::info!(
"Initializing Opus encoder: {}Hz, {} channels",
sample_rate,
channel_count
);
ffmpeg::init()?;
let codec =
ffmpeg::encoder::find(ffmpeg::codec::Id::OPUS).context("Opus encoder not found")?;
tracing::debug!("Found Opus codec: {:?}", codec.name());
let mut ctx = ffmpeg::codec::context::Context::new_with_codec(codec)
.encoder()
.audio()?;
let sample_rate = sample_rate as i32;
ctx.set_rate(sample_rate);
ctx.set_bit_rate(bitrate as usize);
ctx.set_format(ffmpeg::format::Sample::F32(
ffmpeg_next::format::sample::Type::Packed,
));
ctx.set_time_base(Rational::new(1, sample_rate));
ctx.set_channel_layout(if channel_count == 1 {
ffmpeg::util::channel_layout::ChannelLayout::MONO
} else {
ffmpeg::util::channel_layout::ChannelLayout::STEREO
});
let encoder = ctx.open()?;
let extradata = encoder.extradata().unwrap_or(&[]).to_vec();
tracing::info!("Opus encoder initialized successfully");
Ok(Self {
encoder,
frame_count: 0,
sample_rate: sample_rate as u32,
channel_count,
extradata,
bitrate,
})
}
}
impl AudioEncoder for OpusEncoder {
fn with_preset(format: AudioFormat, preset: AudioPreset) -> Result<Self>
where
Self: Sized,
{
let channel_count = format.channel_count;
let bitrate = match preset {
AudioPreset::Hq => BITRATE,
AudioPreset::Lq => 32_000,
};
Self::new(SAMPLE_RATE, channel_count, bitrate)
}
}
impl AudioEncoderInner for OpusEncoder {
fn name(&self) -> &str {
self.encoder.id().name()
}
fn config(&self) -> AudioConfig {
hang::catalog::AudioConfig {
codec: hang::catalog::AudioCodec::Opus,
sample_rate: self.sample_rate,
channel_count: self.channel_count,
bitrate: Some(self.bitrate),
description: Some(self.extradata.clone().into()),
}
}
fn push_samples(&mut self, samples: &[f32]) -> Result<()> {
if samples.is_empty() {
return Ok(());
}
let samples_per_channel = samples.len() / self.channel_count as usize;
debug_assert_eq!(samples_per_channel as u32, self.encoder.frame_size());
let mut audio_frame = ffmpeg::util::frame::Audio::new(
ffmpeg::util::format::sample::Sample::F32(ffmpeg::util::format::sample::Type::Packed),
samples_per_channel,
ffmpeg::util::channel_layout::ChannelLayout::default(self.channel_count as i32),
);
// Copy interleaved samples directly since we're using packed format
let frame_data = audio_frame.data_mut(0);
let frame_samples: &mut [f32] = bytemuck::cast_slice_mut(frame_data);
let copy_len = samples.len().min(frame_samples.len());
frame_samples[..copy_len].copy_from_slice(&samples[..copy_len]);
audio_frame.set_pts(Some(self.frame_count as i64));
self.frame_count += samples_per_channel as u64;
trace!("push samples {}", audio_frame.samples());
self.encoder.send_frame(&audio_frame)?;
Ok(())
}
fn pop_packet(&mut self) -> Result<Option<hang::Frame>> {
let mut packet = ffmpeg::packet::Packet::empty();
match self.encoder.receive_packet(&mut packet) {
Ok(()) => {
let payload = packet.data().unwrap_or(&[]).to_vec();
let hang_frame = hang::Frame {
payload: payload.into(),
timestamp: Timestamp::from_micros(
(self.frame_count * 1_000_000) / self.sample_rate as u64,
)?,
keyframe: true, // Audio frames are generally independent
};
trace!("poll frame {}", hang_frame.payload.num_bytes());
Ok(Some(hang_frame))
}
Err(ffmpeg::Error::Eof) => Ok(None),
Err(ffmpeg::Error::Other { errno }) if errno == ffmpeg::util::error::EAGAIN => Ok(None),
Err(e) => Err(e.into()),
}
}
}

View file

@ -0,0 +1,108 @@
use crate::av::Decoders;
pub use self::{audio::*, ext::ffmpeg_log_init, video::*};
#[derive(Debug, Clone, Copy)]
pub struct FfmpegDecoders;
impl Decoders for FfmpegDecoders {
type Audio = FfmpegAudioDecoder;
type Video = FfmpegVideoDecoder;
}
mod audio {
mod decoder;
mod encoder;
pub use decoder::*;
pub use encoder::*;
}
pub mod video {
mod decoder;
mod encoder;
pub(crate) mod util;
pub use decoder::*;
pub use encoder::*;
}
pub(crate) mod ext {
use buf_list::BufList;
use bytes::Buf;
use ffmpeg_next as ffmpeg;
pub fn ffmpeg_log_init() {
use ffmpeg::util::log::Level::*;
let level = if let Ok(val) = std::env::var("FFMPEG_LOG") {
match val.as_str() {
"quiet" => Quiet,
"panic" => Panic,
"fatal" => Fatal,
"error" => Error,
"warn" | "warning" => Warning,
"info" => Info,
"verbose" => Verbose,
"debug" => Debug,
"trace" => Trace,
_ => Warning,
}
} else {
Warning
};
ffmpeg::util::log::set_level(level);
}
pub trait PacketExt {
fn to_ffmpeg_packet(self) -> ffmpeg::Packet;
}
impl PacketExt for BufList {
fn to_ffmpeg_packet(mut self) -> ffmpeg_next::Packet {
let mut packet = ffmpeg::Packet::new(self.num_bytes());
let dst = packet.data_mut().unwrap();
self.copy_to_slice(dst);
packet
}
}
pub trait CodecContextExt {
fn extradata(&self) -> Option<&[u8]>;
fn set_extradata(&mut self, extradata: &[u8]) -> Result<(), ffmpeg::Error>;
}
impl CodecContextExt for ffmpeg::codec::Context {
// SAFETY: Written by ChatGPT, so, dunno.
fn extradata(&self) -> Option<&[u8]> {
unsafe {
let ctx = self.as_ptr();
if (*ctx).extradata.is_null() || (*ctx).extradata_size <= 0 {
return None;
}
Some(std::slice::from_raw_parts(
(*ctx).extradata as *const u8,
(*ctx).extradata_size as usize,
))
}
}
// SAFETY: Written by ChatGPT, so, dunno.
fn set_extradata(&mut self, extradata: &[u8]) -> Result<(), ffmpeg::Error> {
unsafe {
let ctx = self.as_mut_ptr();
// allocate extradata + padding
let pad = ffmpeg::ffi::AV_INPUT_BUFFER_PADDING_SIZE as usize;
let size = extradata.len() + pad;
(*ctx).extradata = ffmpeg::ffi::av_mallocz(size).cast::<u8>();
if (*ctx).extradata.is_null() {
return Err(ffmpeg::Error::Bug.into());
}
// copy bytes and zero the padding
std::ptr::copy_nonoverlapping(
extradata.as_ptr(),
(*ctx).extradata,
extradata.len(),
);
(*ctx).extradata_size = extradata.len() as i32;
}
Ok(())
}
}
}

View file

@ -0,0 +1,139 @@
use anyhow::{Context, Result};
use ffmpeg_next::{
self as ffmpeg, codec, codec::Id as CodecId, util::frame::video::Video as FfmpegFrame,
};
use crate::{
av::{self, DecodeConfig, DecodedFrame, VideoDecoder},
ffmpeg::{
ext::{CodecContextExt, PacketExt},
video::util::{Rescaler, StreamClock},
},
};
pub struct FfmpegVideoDecoder {
codec: ffmpeg::decoder::Video,
rescaler: Rescaler,
clock: StreamClock,
decoded: FfmpegFrame,
viewport_changed: Option<(u32, u32)>,
last_timestamp: Option<hang::Timestamp>,
}
impl VideoDecoder for FfmpegVideoDecoder {
fn name(&self) -> &str {
self.codec.id().name()
}
fn new(config: &hang::catalog::VideoConfig, playback_config: &DecodeConfig) -> Result<Self>
where
Self: Sized,
{
ffmpeg::init()?;
// Build a decoder context for H.264 and attach extradata (e.g., avcC)
let codec = match &config.codec {
hang::catalog::VideoCodec::H264(_meta) => {
let codec =
codec::decoder::find(CodecId::H264).context("H.264 decoder not found")?;
let mut ctx = codec::context::Context::new_with_codec(codec);
if let Some(description) = &config.description {
ctx.set_extradata(&description)?;
}
ctx.decoder().video().unwrap()
}
hang::catalog::VideoCodec::AV1(_meta) => {
let codec = codec::decoder::find(CodecId::AV1).context("AV1 decoder not found")?;
let mut ctx = codec::context::Context::new_with_codec(codec);
if let Some(description) = &config.description {
ctx.set_extradata(&description)?;
}
ctx.decoder().video().unwrap()
}
_ => anyhow::bail!(
"Unsupported codec {} (only h264 and av1 are supported)",
config.codec
),
};
let rescaler = Rescaler::new(playback_config.pixel_format.to_ffmpeg(), None)?;
let clock = StreamClock::default();
let decoded = FfmpegFrame::empty();
Ok(Self {
codec,
rescaler,
clock,
decoded,
viewport_changed: None,
last_timestamp: None,
})
}
fn set_viewport(&mut self, w: u32, h: u32) {
self.viewport_changed = Some((w, h));
}
fn push_packet(&mut self, packet: hang::Frame) -> Result<()> {
let ffmpeg_packet = packet.payload.to_ffmpeg_packet();
self.codec.send_packet(&ffmpeg_packet)?;
self.last_timestamp = Some(packet.timestamp);
Ok(())
}
fn pop_frame(&mut self) -> Result<Option<av::DecodedFrame>> {
// Pull all available decoded frames
match self.codec.receive_frame(&mut self.decoded) {
Ok(()) => {
// Apply clamped target size.
if let Some((max_width, max_height)) = self.viewport_changed.take() {
let (width, height) =
calculate_resized_size(&self.decoded, max_width, max_height);
self.rescaler.set_target_dimensions(width, height);
}
let frame = self.rescaler.process(&mut self.decoded)?;
let last_timestamp = self
.last_timestamp
.as_ref()
.context("missing last packet")?;
let frame = DecodedFrame::from_ffmpeg(
frame,
self.clock.frame_delay(&last_timestamp),
std::time::Duration::from(*last_timestamp),
);
Ok(Some(frame))
}
Err(ffmpeg::util::error::Error::BufferTooSmall) => Ok(None),
Err(ffmpeg::Error::Other { errno }) if errno == ffmpeg::util::error::EAGAIN => Ok(None),
Err(err) => {
// tracing::warn!("decoder error: {err} {err:?} {err:#?}");
// Ok(None)
Err(err.into())
}
}
}
}
/// Calculates the target frame size to fit into the requested bounds while preserving aspect ratio.
fn calculate_resized_size(decoded: &FfmpegFrame, max_width: u32, max_height: u32) -> (u32, u32) {
let src_w = decoded.width().max(1);
let src_h = decoded.height().max(1);
let max_w = max_width.max(1);
let max_h = max_height.max(1);
// Fit within requested bounds, preserve aspect ratio, never upscale
let scale_w = (max_w as f32) / (src_w as f32);
let scale_h = (max_h as f32) / (src_h as f32);
let scale = scale_w.min(scale_h).min(1.0).max(0.0);
let target_width = ((src_w as f32) * scale).floor().max(1.0) as u32;
let target_height = ((src_h as f32) * scale).floor().max(1.0) as u32;
tracing::debug!(
src_w,
src_h,
max_w,
max_h,
target_width,
target_height,
"scale"
);
(target_width, target_height)
}

View file

@ -0,0 +1,568 @@
use std::{
ffi::{CString, c_int},
ptr,
task::Poll,
};
use anyhow::{Context, Result, anyhow};
use ffmpeg_next::{self as ffmpeg, codec, format::Pixel, frame::Video as VideoFrame};
use hang::Timestamp;
use tracing::{debug, info, trace};
use crate::{
av,
ffmpeg::{ext::CodecContextExt, util::Rescaler},
};
#[derive(Debug, Clone, Copy, Default)]
// Allow unused because usage is cfg-gated on platform.
#[allow(unused)]
enum HwBackend {
#[default]
Software,
/// Linux
Vaapi,
/// macOS
Videotoolbox,
/// Nvidia GPUs
Nvenc,
/// Intel GPUs
Qsv,
/// AMD GPUs
Amf,
// TODO:
// Add DirectX (Windows)
// Add MediaCodec (Android)
}
impl HwBackend {
fn codec_name(&self) -> &'static str {
match self {
Self::Software => "libx264",
Self::Vaapi => "h264_vaapi",
Self::Videotoolbox => "h264_videotoolbox",
Self::Nvenc => "h264_nvenc",
Self::Qsv => "h264_qsv",
Self::Amf => "h264_amf",
}
}
fn candidates() -> Vec<Self> {
// vec![HwBackend::Software]
let mut candidates = Vec::new();
// Platform-preferred order
#[cfg(target_os = "macos")]
candidates.extend_from_slice(&[HwBackend::Videotoolbox]);
#[cfg(target_os = "windows")]
candidates.extend_from_slice(&[HwBackend::Nvenc, HwBackend::Qsv, HwBackend::Amf]);
#[cfg(target_os = "linux")]
candidates.extend_from_slice(&[HwBackend::Vaapi, HwBackend::Nvenc, HwBackend::Qsv]);
// Always end with software
candidates.push(HwBackend::Software);
candidates
}
fn pixel_format(&self) -> Pixel {
match self {
HwBackend::Vaapi | HwBackend::Qsv => Pixel::NV12,
// These rest accepts yuv420p SW frames:
_ => Pixel::YUV420P,
}
}
fn hardware_pixel_format(&self) -> Pixel {
match self {
HwBackend::Vaapi => Pixel::VAAPI,
HwBackend::Qsv => Pixel::NV12,
// These rest accepts yuv420p SW frames:
_ => Pixel::YUV420P,
}
}
}
#[derive(Debug, Clone)]
struct EncoderOpts {
width: u32,
height: u32,
framerate: u32,
bitrate: u64,
}
pub struct H264Encoder {
encoder: ffmpeg::encoder::video::Encoder,
rescaler: Rescaler,
backend: HwBackend,
vaapi: Option<VaapiState>,
opts: EncoderOpts,
frame_count: u64,
}
impl H264Encoder {
pub fn new(width: u32, height: u32, framerate: u32) -> Result<Self> {
info!("Initializing H264 encoder: {width}x{height} @ {framerate}fps");
ffmpeg::init()?;
// Bitrate heuristic (from your original)
let pixels = width * height;
let framerate_factor = 30.0 + (framerate as f32 - 30.) / 2.;
let bitrate = (pixels as f32 * 0.07 * framerate_factor).round() as u64;
let opts = EncoderOpts {
width,
height,
framerate,
bitrate,
};
let candidates = HwBackend::candidates();
// Try each backend
let mut last_err: Option<anyhow::Error> = None;
for backend in candidates {
match Self::open_encoder(backend, &opts) {
Ok((encoder, rescaler, vaapi)) => {
info!(
"Using encoder backend: {} ({backend:?})",
backend.codec_name()
);
return Ok(Self {
encoder,
rescaler,
vaapi,
backend,
opts,
frame_count: 0,
});
}
Err(e) => {
debug!(
"Backend {backend:?} ({}) not available: {e:#}",
backend.codec_name()
);
last_err = Some(e);
}
}
}
Err(last_err.unwrap_or_else(|| anyhow!("no H.264 encoder available")))
}
fn open_encoder(
backend: HwBackend,
opts: &EncoderOpts,
) -> Result<(
ffmpeg::encoder::video::Encoder,
Rescaler,
Option<VaapiState>,
)> {
// Find encoder
let codec = ffmpeg::codec::encoder::find_by_name(backend.codec_name())
.with_context(|| format!("encoder {} not found", backend.codec_name()))?;
debug!("Found encoder: {}", codec.name());
// Build ctx
let mut ctx = codec::context::Context::new_with_codec(codec);
unsafe {
let ctx_mut = ctx.as_mut_ptr();
(*ctx_mut).width = opts.width as i32;
(*ctx_mut).height = opts.height as i32;
(*ctx_mut).time_base.num = 1;
(*ctx_mut).time_base.den = opts.framerate as i32;
(*ctx_mut).framerate.num = opts.framerate as i32;
(*ctx_mut).framerate.den = 1;
(*ctx_mut).gop_size = opts.framerate as i32;
(*ctx_mut).bit_rate = opts.bitrate as i64;
(*ctx_mut).flags = (*ctx_mut).flags | codec::Flags::GLOBAL_HEADER.bits() as c_int;
(*ctx_mut).pix_fmt = backend.hardware_pixel_format().into();
}
// Backend-specific prep
let vaapi_state = if matches!(backend, HwBackend::Vaapi) {
// single-GPU default; make configurable if needed
let va = VaapiState::new(opts.width, opts.height, "/dev/dri/renderD128")?;
va.bind_to_context(&mut ctx);
Some(va)
} else {
None
};
// Setup encoder options
let enc_opts = {
let mut opts = vec![
// Disable annexB so that we get an avcC header in extradata
// annexb=0 → MP4/ISO BMFF style (length-prefixed NAL units + avcC extradata),
// as opposed to Annex B start codes (00 00 00 01).
("annexB", "0"),
];
if matches!(backend, HwBackend::Software) {
opts.extend_from_slice(&[
("preset", "ultrafast"),
("tune", "zerolatency"),
("profile", "baseline"),
]);
}
ffmpeg::Dictionary::from_iter(opts.into_iter())
};
// Open encoder
let encoder = ctx.encoder().video()?.open_as_with(codec, enc_opts)?;
// Build rescaler to SW input fmt expected per-backend
let rescaler = Rescaler::new(backend.pixel_format(), Some((opts.width, opts.height)))?;
Ok((encoder, rescaler, vaapi_state))
}
pub fn video_config(&self) -> Result<hang::catalog::VideoConfig> {
Ok(hang::catalog::VideoConfig {
codec: hang::catalog::VideoCodec::H264(hang::catalog::H264 {
profile: 0x42, // Baseline
constraints: 0xE0,
level: 0x1E, // Level 3.0
inline: false, // TODO: is this correct?
}),
description: Some(self.avcc_description()?.to_vec().into()),
coded_width: Some(self.opts.width),
coded_height: Some(self.opts.height),
display_ratio_width: None,
display_ratio_height: None,
bitrate: Some(self.opts.bitrate),
framerate: Some(self.opts.framerate as f64),
optimize_for_latency: Some(true),
})
}
pub fn avcc_description(&self) -> Result<&[u8]> {
self.encoder.extradata().context("missing avcC extradata")
}
pub fn receive_packet(&mut self) -> Result<Poll<Option<hang::Frame>>> {
loop {
let mut packet = ffmpeg::packet::Packet::empty();
match self.encoder.receive_packet(&mut packet) {
Ok(()) => {
let payload = packet.data().unwrap_or(&[]).to_vec();
let hang_frame = hang::Frame {
payload: payload.into(),
timestamp: Timestamp::from_micros(
self.frame_count * 1_000_000 / self.opts.framerate as u64,
)?,
keyframe: packet.is_key(),
};
return Ok(Poll::Ready(Some(hang_frame)));
}
Err(ffmpeg::Error::Eof) => return Ok(Poll::Ready(None)),
Err(ffmpeg::Error::Other { errno }) if errno == ffmpeg::util::error::EAGAIN => {
return Ok(Poll::Pending);
}
Err(e) => return Err(e.into()),
}
}
}
pub fn encode_frame(&mut self, mut frame: VideoFrame) -> Result<()> {
frame.set_pts(Some(self.frame_count as i64));
self.frame_count += 1;
if self.frame_count % self.opts.framerate as u64 == 0 {
tracing::trace!(
"Encoding {}: {}x{} fmt={:?} pts={:?} backend={:?}",
self.frame_count,
frame.width(),
frame.height(),
frame.format(),
frame.pts(),
self.backend
);
}
// Convert incoming frame to the SW format the backend expects.
let frame = self
.rescaler
.process(&frame)
.context("failed to color-convert frame")?
.clone();
let frame = match self.backend {
HwBackend::Vaapi => {
let va = self
.vaapi
.as_ref()
.ok_or_else(|| anyhow!("no vaapi state"))?;
let hw_frame = va.transfer_nv12_to_hw(&frame)?;
hw_frame
}
// Other backends accept SW frames directly
_ => frame,
};
self.encoder
.send_frame(&frame)
.map_err(|e| anyhow!("send_frame failed: {e:?}"))?;
Ok(())
}
pub fn flush(&mut self) -> Result<()> {
self.encoder.send_eof()?;
Ok(())
}
}
impl av::VideoEncoder for H264Encoder {
fn with_preset(preset: av::VideoPreset) -> Result<Self>
where
Self: Sized,
{
Self::new(preset.width(), preset.height(), preset.fps())
}
}
impl av::VideoEncoderInner for H264Encoder {
fn name(&self) -> &str {
self.encoder.id().name()
}
fn config(&self) -> hang::catalog::VideoConfig {
self.video_config().expect("video_config available")
}
fn push_frame(&mut self, frame: av::VideoFrame) -> anyhow::Result<()> {
trace!(len = frame.raw.len(), format=?frame.format, "push frame");
let frame = frame.to_ffmpeg();
self.encode_frame(frame)
}
fn pop_packet(&mut self) -> anyhow::Result<Option<hang::Frame>> {
match self.receive_packet()? {
std::task::Poll::Ready(v) => Ok(v),
std::task::Poll::Pending => Ok(None),
}
}
}
struct VaapiState {
device_ctx: *mut ffmpeg::sys::AVBufferRef,
frames_ctx: *mut ffmpeg::sys::AVBufferRef,
}
unsafe impl Send for VaapiState {}
impl VaapiState {
/// Create VAAPI device + frames pool (NV12→VAAPI surfaces) for given size.
fn new(width: u32, height: u32, device_path: &str) -> Result<Self> {
// 1) Create VAAPI device
let cpath = CString::new(device_path)?;
let mut dev: *mut ffmpeg::sys::AVBufferRef = ptr::null_mut();
let ret = unsafe {
ffmpeg::sys::av_hwdevice_ctx_create(
&mut dev,
ffmpeg::sys::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI,
cpath.as_ptr(),
ptr::null_mut(),
0,
)
};
if ret < 0 || dev.is_null() {
unsafe { ffmpeg::sys::av_buffer_unref(&mut dev) };
return Err(anyhow!("vaapi device create failed: {ret}"));
}
// 2) Create frames pool for VAAPI with SW format NV12
let frames = unsafe { ffmpeg::sys::av_hwframe_ctx_alloc(dev) };
if frames.is_null() {
unsafe { ffmpeg::sys::av_buffer_unref(&mut dev) };
return Err(anyhow!("av_hwframe_ctx_alloc failed"));
}
let fc = unsafe { &mut *((*frames).data as *mut ffmpeg::sys::AVHWFramesContext) };
fc.format = ffmpeg::sys::AVPixelFormat::AV_PIX_FMT_VAAPI;
fc.sw_format = ffmpeg::sys::AVPixelFormat::AV_PIX_FMT_NV12;
fc.width = width as i32;
fc.height = height as i32;
fc.initial_pool_size = 32;
let ret = unsafe { ffmpeg::sys::av_hwframe_ctx_init(frames) };
if ret < 0 {
unsafe {
ffmpeg::sys::av_buffer_unref(&mut (frames as *mut _));
ffmpeg::sys::av_buffer_unref(&mut dev);
}
return Err(anyhow!("av_hwframe_ctx_init failed: {ret}"));
}
Ok(Self {
device_ctx: dev,
frames_ctx: frames,
})
}
/// Attach the frames context so the codec context expects VAAPI frames.
fn bind_to_context(&self, ctx: &mut codec::context::Context) {
unsafe {
let ctx = ctx.as_mut_ptr();
(*ctx).hw_frames_ctx = ffmpeg::sys::av_buffer_ref(self.frames_ctx);
(*ctx).pix_fmt = Pixel::VAAPI.into();
}
}
/// Transfer a SW NV12 frame into a VAAPI HW frame, preserving PTS.
/// Returns a new `VideoFrame` backed by a VAAPI surface.
fn transfer_nv12_to_hw(&self, sw_frame: &VideoFrame) -> Result<VideoFrame> {
unsafe {
// Allocate an empty HW frame from the pool
let mut hw = ffmpeg::frame::Video::empty();
let ret = ffmpeg::sys::av_hwframe_get_buffer(self.frames_ctx, hw.as_mut_ptr(), 0);
if ret < 0 {
return Err(anyhow!("av_hwframe_get_buffer failed: {ret}"));
}
// Keep PTS
(*hw.as_mut_ptr()).pts = sw_frame.pts().unwrap_or(0);
// Transfer SW NV12 → HW VAAPI surface
let ret = ffmpeg::sys::av_hwframe_transfer_data(hw.as_mut_ptr(), sw_frame.as_ptr(), 0);
if ret < 0 {
return Err(anyhow!("av_hwframe_transfer_data failed: {ret}"));
}
Ok(hw)
}
}
}
impl Drop for VaapiState {
fn drop(&mut self) {
unsafe {
if !self.frames_ctx.is_null() {
ffmpeg::sys::av_buffer_unref(&mut (self.frames_ctx as *mut _));
}
if !self.device_ctx.is_null() {
ffmpeg::sys::av_buffer_unref(&mut (self.device_ctx as *mut _));
}
}
}
}
// pub struct Av1FfmpegEncoder {
// encoder: ffmpeg::encoder::video::Encoder,
// rescaler: Rescaler,
// opts: EncoderOpts,
// frame_count: u64,
// }
// impl Av1FfmpegEncoder {
// pub fn new(width: u32, height: u32, framerate: u32) -> Result<Self> {
// info!("Initializing AV1 (FFmpeg) encoder: {width}x{height} @ {framerate}fps");
// ffmpeg::init()?;
// let pixels = width * height;
// let framerate_factor = 30.0 + (framerate as f32 - 30.) / 2.;
// let bitrate = (pixels as f32 * 0.05 * framerate_factor).round() as u64;
// let opts = EncoderOpts {
// width,
// height,
// framerate,
// bitrate,
// };
// let codec = ffmpeg::encoder::find(ffmpeg::codec::Id::AV1).context("AV1 codec not found")?;
// let mut ctx = codec::context::Context::new_with_codec(codec);
// unsafe {
// let ctx_mut = ctx.as_mut_ptr();
// (*ctx_mut).width = width as i32;
// (*ctx_mut).height = height as i32;
// (*ctx_mut).time_base.num = 1;
// (*ctx_mut).time_base.den = framerate as i32;
// (*ctx_mut).framerate.num = framerate as i32;
// (*ctx_mut).framerate.den = 1;
// (*ctx_mut).gop_size = framerate as i32;
// (*ctx_mut).bit_rate = bitrate as i64;
// (*ctx_mut).pix_fmt = Pixel::YUV420P.into();
// }
// // libaom options for realtime
// let enc_opts =
// ffmpeg::Dictionary::from_iter([("cpu-used", "8"), ("row-mt", "1"), ("tiles", "2x2")]);
// let encoder = ctx.encoder().video()?.open_as_with(
// ffmpeg::encoder::find(ffmpeg::codec::Id::AV1).unwrap(),
// enc_opts,
// )?;
// let rescaler = Rescaler::new(Pixel::YUV420P, Some((width, height)))?;
// Ok(Self {
// encoder,
// rescaler,
// opts,
// frame_count: 0,
// })
// }
// }
// impl av::VideoEncoder for Av1FfmpegEncoder {
// fn with_preset(preset: av::VideoPreset) -> Result<Self>
// where
// Self: Sized,
// {
// Self::new(preset.width(), preset.height(), preset.fps())
// }
// fn config(&self) -> hang::catalog::VideoConfig {
// hang::catalog::VideoConfig {
// codec: hang::catalog::VideoCodec::AV1(Default::default()),
// description: None,
// coded_width: Some(self.opts.width),
// coded_height: Some(self.opts.height),
// display_ratio_width: None,
// display_ratio_height: None,
// bitrate: Some(self.opts.bitrate),
// framerate: Some(self.opts.framerate as f64),
// optimize_for_latency: Some(true),
// }
// }
// fn push_frame(
// &mut self,
// format: &av::VideoFormat,
// frame: av::VideoFrame,
// ) -> anyhow::Result<()> {
// use ffmpeg_next::frame::Video as FfFrame;
// let pixel = match format.pixel_format {
// av::PixelFormat::Rgba => Pixel::RGBA,
// av::PixelFormat::Bgra => Pixel::BGRA,
// };
// let [w, h] = format.dimensions;
// let mut ff = FfFrame::new(pixel, w, h);
// let stride = ff.stride(0) as usize;
// let row_bytes = (w as usize) * 4;
// for y in 0..(h as usize) {
// let dst_off = y * stride;
// let src_off = y * row_bytes;
// ff.data_mut(0)[dst_off..dst_off + row_bytes]
// .copy_from_slice(&frame.raw[src_off..src_off + row_bytes]);
// }
// let sw = self
// .rescaler
// .process(&ff)
// .context("failed to color-convert frame")?
// .clone();
// let mut enc_frame = sw;
// enc_frame.set_pts(Some(self.frame_count as i64));
// self.frame_count += 1;
// self.encoder.send_frame(&enc_frame)?;
// Ok(())
// }
// fn pop_packet(&mut self) -> anyhow::Result<Option<hang::Frame>> {
// let mut packet = ffmpeg::packet::Packet::empty();
// match self.encoder.receive_packet(&mut packet) {
// Ok(()) => {
// let payload = packet.data().unwrap_or(&[]).to_vec();
// let hang_frame = hang::Frame {
// payload: payload.into(),
// timestamp: std::time::Duration::from_nanos(
// self.frame_count.saturating_sub(1) * 1_000_000_000
// / self.opts.framerate as u64,
// ),
// keyframe: packet.is_key(),
// };
// Ok(Some(hang_frame))
// }
// Err(ffmpeg::Error::Eof) => Ok(None),
// Err(ffmpeg::Error::Other { errno }) if errno == ffmpeg::util::error::EAGAIN => Ok(None),
// Err(e) => Err(e.into()),
// }
// }
// }

View file

@ -0,0 +1,138 @@
use std::time::Duration;
use bytes::{BufMut, BytesMut};
use ffmpeg_next::util::{format::pixel::Pixel, frame::video::Video as FfmpegFrame};
use hang::Timestamp;
use image::{Delay, RgbaImage};
pub(crate) use self::mjpg_decoder::MjpgDecoder;
pub(crate) use self::rescaler::Rescaler;
use crate::av::{self, DecodedFrame, PixelFormat, VideoFormat, VideoFrame};
mod mjpg_decoder;
mod rescaler;
#[derive(Default, Debug)]
pub(crate) struct StreamClock {
pub(crate) last_timestamp: Option<hang::Timestamp>,
}
impl StreamClock {
pub(crate) fn frame_delay(&mut self, timestamp: &hang::Timestamp) -> Duration {
// Compute interframe delay from provided timestamps
let delay = match self.last_timestamp {
None => Duration::ZERO,
Some(last_timestamp) => timestamp
.checked_sub(last_timestamp)
.unwrap_or(Timestamp::ZERO)
.into(),
};
self.last_timestamp = Some(*timestamp);
delay
}
}
impl av::VideoFrame {
pub fn to_ffmpeg(&self) -> FfmpegFrame {
// Wrap raw RGBA/BGRA data into an ffmpeg frame and encode
let pixel = match self.format.pixel_format {
av::PixelFormat::Rgba => Pixel::RGBA,
av::PixelFormat::Bgra => Pixel::BGRA,
};
let [w, h] = self.format.dimensions;
let mut ff = FfmpegFrame::new(pixel, w, h);
let stride = ff.stride(0) as usize;
let row_bytes = (w as usize) * 4;
for y in 0..(h as usize) {
let dst_off = y * stride;
let src_off = y * row_bytes;
ff.data_mut(0)[dst_off..dst_off + row_bytes]
.copy_from_slice(&self.raw[src_off..src_off + row_bytes]);
}
ff
}
}
impl av::DecodedFrame {
pub fn from_ffmpeg(frame: &FfmpegFrame, delay: Duration, timestamp: Duration) -> Self {
let image = ffmpeg_frame_to_image(frame);
// Compute interframe delay from provided timestamps
let delay = Delay::from_saturating_duration(delay);
DecodedFrame {
frame: image::Frame::from_parts(image, 0, 0, delay),
timestamp,
}
}
}
/// Convert the ffmpeg frame into an [image] frame.
///
/// Note: This does not do any color conversion. Make sure the frame is in the correct color format before.
///
/// This allocates the full frame into a vec, which we need anyway to cross the thread boundary.
pub(crate) fn ffmpeg_frame_to_image(frame: &ffmpeg_next::util::frame::Video) -> image::RgbaImage {
let width = frame.width();
let height = frame.height();
let bytes_per_pixel = 4usize; // BGRA
let src = frame.data(0);
// ffmpeg frames may have padding at end of each line; copy row-by-row.
let stride = frame.stride(0) as usize;
let row_bytes = (width as usize) * bytes_per_pixel;
let mut out = vec![0u8; row_bytes * (height as usize)];
for y in 0..(height as usize) {
let src_off = y * stride;
let dst_off = y * row_bytes;
out[dst_off..dst_off + row_bytes].copy_from_slice(&src[src_off..src_off + row_bytes]);
}
RgbaImage::from_raw(width, height, out).expect("valid image buffer")
}
impl PixelFormat {
pub fn to_ffmpeg(&self) -> Pixel {
match self {
PixelFormat::Rgba => Pixel::RGBA,
PixelFormat::Bgra => Pixel::BGRA,
}
}
pub fn from_ffmpeg(value: Pixel) -> Option<Self> {
match value {
Pixel::RGBA => Some(PixelFormat::Rgba),
Pixel::BGRA => Some(PixelFormat::Bgra),
_ => None,
}
}
}
/// Convert the ffmpeg frame into a [`VideoFrame`]
///
/// Returns `None` if the frame has an unsupported pixel format.
///
/// This allocates the full frame into a vec, which we need anyway to cross the thread boundary.
pub(crate) fn ffmpeg_frame_to_video_frame(
frame: &ffmpeg_next::util::frame::Video,
) -> Option<VideoFrame> {
let pixel_format = PixelFormat::from_ffmpeg(frame.format())?;
let width = frame.width();
let height = frame.height();
let bytes_per_pixel = 4usize; // RGBA/BGRA
let src = frame.data(0);
// ffmpeg frames may have padding at end of each line; copy row-by-row.
let stride = frame.stride(0) as usize;
let row_bytes = (width as usize) * bytes_per_pixel;
let mut out = BytesMut::with_capacity(row_bytes * (height as usize));
// let mut out = vec![0u8; row_bytes * (height as usize)];
for y in 0..(height as usize) {
let src_off = y * stride;
// let dst_off = y * row_bytes;
out.put(&src[src_off..src_off + row_bytes]);
// out[dst_off..dst_off + row_bytes].copy_from_slice(&src[src_off..src_off + row_bytes]);
}
Some(VideoFrame {
format: VideoFormat {
dimensions: [width, height],
pixel_format,
},
raw: out.freeze(),
})
}

View file

@ -0,0 +1,74 @@
use std::time::Instant;
use ffmpeg_next::{
self as ffmpeg, Error, Packet, codec::Id, format::Pixel, frame::Video as FfmpegVideoFrame,
};
use tracing::trace;
use crate::{
av::VideoFrame,
ffmpeg::util::{Rescaler, ffmpeg_frame_to_video_frame},
};
pub struct MjpgDecoder {
dec: ffmpeg::decoder::Video,
rescaler: Rescaler,
}
impl MjpgDecoder {
/// Initialize FFmpeg and create a Video decoder for MJPEG.
pub fn new() -> anyhow::Result<Self> {
ffmpeg::init()?;
// Find the MJPEG decoder and create a context bound to it.
let mjpeg = ffmpeg::decoder::find(Id::MJPEG).ok_or(Error::DecoderNotFound)?;
// Create a codec::Context that's pre-bound to this decoder codec,
// then get a video decoder out of it.
let ctx = ffmpeg::codec::context::Context::new_with_codec(mjpeg);
let dec = ctx.decoder().video()?; // has send_packet/receive_frame
let rescaler = Rescaler::new(Pixel::RGBA, None)?;
Ok(Self { dec, rescaler })
}
/// Decode one complete MJPEG/JPEG frame from `mjpg_frame`.
pub fn decode_frame(&mut self, mjpg_frame: &[u8]) -> Result<VideoFrame, Error> {
let now = Instant::now();
// Make a packet that borrows/copies the data.
let packet = Packet::borrow(mjpg_frame);
// Feed & drain once — MJPEG is intra-only (one picture per packet).
self.dec.send_packet(&packet)?;
trace!(t=?now.elapsed(), "decode ffmpeg: send packet");
let mut frame = FfmpegVideoFrame::empty();
self.dec.receive_frame(&mut frame)?;
trace!(t=?now.elapsed(), "decode ffmpeg: receive frame");
// MJPEG may output deprecated YUVJ* formats. Replace them with
// the non-deprecated equivalents and mark full range to keep semantics.
// This avoids ffmpeg warning: "deprecated pixel format used, make sure you did set range correctly".
use ffmpeg_next::util::color::Range;
match frame.format() {
Pixel::YUVJ420P => {
frame.set_color_range(Range::JPEG);
frame.set_format(Pixel::YUV420P);
}
Pixel::YUVJ422P => {
frame.set_color_range(Range::JPEG);
frame.set_format(Pixel::YUV422P);
}
Pixel::YUVJ444P => {
frame.set_color_range(Range::JPEG);
frame.set_format(Pixel::YUV444P);
}
_ => {}
}
trace!(t=?now.elapsed(), "decode ffmpeg: color");
let frame = self.rescaler.process(&frame)?;
trace!(t=?now.elapsed(), "decode ffmpeg: rescale");
let frame = ffmpeg_frame_to_video_frame(frame).expect("valid pixel format set in rescaler");
trace!(t=?now.elapsed(), "decode ffmpeg: convert");
Ok(frame)
}
}

View file

@ -0,0 +1,77 @@
use anyhow::Result;
use ffmpeg_next::software::scaling::Flags;
use ffmpeg_next::{
self as ffmpeg,
software::scaling::{self},
util::{format::pixel::Pixel, frame::video::Video as FfmpegFrame},
};
pub(crate) struct Rescaler {
pub(crate) target_format: Pixel,
pub(crate) target_width_height: Option<(u32, u32)>,
pub(crate) ctx: Option<scaling::Context>,
pub(crate) out_frame: FfmpegFrame,
}
// I think the ffmpeg structs are send-safe.
// We want to create the encoder before moving it to a thread.
unsafe impl Send for Rescaler {}
impl Rescaler {
pub fn new(target_format: Pixel, target_width_height: Option<(u32, u32)>) -> Result<Self> {
Ok(Self {
target_format,
ctx: None,
target_width_height,
out_frame: FfmpegFrame::empty(),
})
}
pub fn set_target_dimensions(&mut self, w: u32, h: u32) {
self.target_width_height = Some((w, h));
}
pub fn process<'a: 'b, 'b>(
&'a mut self,
frame: &'b FfmpegFrame,
) -> Result<&'b FfmpegFrame, ffmpeg::Error> {
// Short-circuit if possible.
if self.target_width_height.is_none() && self.target_format == frame.format() {
return Ok(frame);
}
let (target_width, target_height) = self
.target_width_height
.unwrap_or_else(|| (frame.width(), frame.height()));
let out_frame_needs_reset = self.out_frame.width() != target_width
|| self.out_frame.height() != target_height
|| self.out_frame.format() != self.target_format;
if out_frame_needs_reset {
self.out_frame = FfmpegFrame::new(self.target_format, target_width, target_height);
}
let ctx = match self.ctx {
None => self.ctx.insert(scaling::Context::get(
frame.format(),
frame.width(),
frame.height(),
self.out_frame.format(),
self.out_frame.width(),
self.out_frame.height(),
Flags::BILINEAR,
)?),
Some(ref mut ctx) => ctx,
};
// This resets the context if any parameters changed.
ctx.cached(
frame.format(),
frame.width(),
frame.height(),
self.out_frame.format(),
self.out_frame.width(),
self.out_frame.height(),
Flags::BILINEAR,
);
ctx.run(&frame, &mut self.out_frame)?;
Ok(&self.out_frame)
}
}

View file

@ -0,0 +1,9 @@
pub mod audio;
pub mod av;
pub mod capture;
pub mod ffmpeg;
pub mod publish;
pub mod subscribe;
mod util;
pub use audio::AudioBackend;

View file

@ -0,0 +1,594 @@
use std::{
collections::{BTreeMap, HashMap},
sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicU32, Ordering},
},
time::{Duration, Instant},
};
use anyhow::Context;
use hang::catalog::{AudioConfig, Catalog, CatalogProducer, VideoConfig};
use moq_lite::BroadcastProducer;
use n0_error::Result;
use n0_future::task::AbortOnDropHandle;
use tokio_util::sync::{CancellationToken, DropGuard};
use tracing::{debug, error, info, info_span, trace, warn};
use crate::{
av::{
AudioEncoder, AudioEncoderInner, AudioPreset, AudioSource, DecodeConfig, VideoEncoder,
VideoEncoderInner, VideoPreset, VideoSource,
},
subscribe::WatchTrack,
util::spawn_thread,
};
pub struct PublishBroadcast {
producer: BroadcastProducer,
catalog: CatalogProducer,
state: Arc<Mutex<State>>,
_task: Arc<AbortOnDropHandle<()>>,
}
impl PublishBroadcast {
pub fn new() -> Self {
let mut producer = BroadcastProducer::default();
let catalog = Catalog::default().produce();
producer.insert_track(catalog.consumer.track);
let catalog = catalog.producer;
let state = Arc::new(Mutex::new(State::default()));
let task_handle = tokio::spawn(Self::run(state.clone(), producer.clone()));
Self {
producer,
catalog,
state,
_task: Arc::new(AbortOnDropHandle::new(task_handle)),
}
}
pub fn producer(&self) -> BroadcastProducer {
self.producer.clone()
}
async fn run(state: Arc<Mutex<State>>, mut producer: BroadcastProducer) {
while let Some(track) = producer.requested_track().await {
let name = track.info.name.clone();
if state
.lock()
.expect("poisoned")
.start_track(track.clone())
.inspect_err(|err| warn!(%name, "failed to start requested track: {err:#}"))
.is_ok()
{
info!("started track: {name}");
tokio::spawn({
let state = state.clone();
async move {
track.unused().await;
info!("stopping track: {name}");
state.lock().expect("poisoned").stop_track(&name);
}
});
}
}
}
/// Create a local WatchTrack from the current video source, if present.
pub fn watch_local(&self, decode_config: DecodeConfig) -> Option<WatchTrack> {
let (source, shutdown) = {
let state = self.state.lock().expect("poisoned");
let source = state
.available_video
.as_ref()
.map(|video| video.source.clone())?;
Some((source, state.shutdown_token.child_token()))
}?;
Some(WatchTrack::from_video_source(
"local".to_string(),
shutdown,
source,
decode_config,
))
}
pub fn set_video(&mut self, renditions: Option<VideoRenditions>) -> Result<()> {
match renditions {
Some(renditions) => {
let priority = 1u8;
let configs = renditions.available_renditions()?;
let video = hang::catalog::Video {
renditions: configs,
priority,
display: None,
rotation: None,
flip: None,
};
{
let mut catalog = self.catalog.lock();
catalog.video = Some(video);
}
self.state.lock().expect("poisoned").available_video = Some(renditions);
// TODO: Drop active encodings if their rendition is no longer available?
}
None => {
// Clear catalog and stop any active video encoders
self.state.lock().expect("poisoned").remove_video();
{
let mut catalog = self.catalog.lock();
catalog.video = None;
}
}
}
Ok(())
}
pub fn set_audio(&mut self, renditions: Option<AudioRenditions>) -> Result<()> {
match renditions {
Some(renditions) => {
let priority = 2u8;
let configs = renditions.available_renditions()?;
let audio = hang::catalog::Audio {
renditions: configs,
priority,
};
{
let mut catalog = self.catalog.lock();
catalog.audio = Some(audio);
}
self.state.lock().expect("poisoned").available_audio = Some(renditions);
}
None => {
// Clear catalog and stop any active audio encoders
self.state.lock().expect("poisoned").remove_audio();
{
let mut catalog = self.catalog.lock();
catalog.audio = None;
}
}
}
Ok(())
}
}
impl Drop for PublishBroadcast {
fn drop(&mut self) {
self.state.lock().expect("poisoned").shutdown_token.cancel();
self.producer.close();
}
}
#[derive(Default)]
struct State {
shutdown_token: CancellationToken,
available_video: Option<VideoRenditions>,
available_audio: Option<AudioRenditions>,
active_video: HashMap<String, EncoderThread>,
active_audio: HashMap<String, EncoderThread>,
}
impl State {
fn stop_track(&mut self, name: &str) {
let thread = self
.active_video
.remove(name)
.or_else(|| self.active_audio.remove(name));
if let Some(thread) = thread {
thread.shutdown.cancel();
}
}
fn remove_audio(&mut self) {
for (_name, thread) in self.active_audio.drain() {
thread.shutdown.cancel();
}
self.available_audio = None;
}
fn remove_video(&mut self) {
for (_name, thread) in self.active_video.drain() {
thread.shutdown.cancel();
}
self.available_video = None;
}
fn start_track(&mut self, track: moq_lite::TrackProducer) -> Result<()> {
let name = track.info.name.clone();
let track = hang::TrackProducer::new(track);
let shutdown_token = self.shutdown_token.child_token();
if let Some(video) = self.available_video.as_mut()
&& video.contains_rendition(&name)
{
let thread = video.start_encoder(&name, track, shutdown_token)?;
self.active_video.insert(name, thread);
Ok(())
} else if let Some(audio) = self.available_audio.as_mut()
&& audio.contains_rendition(&name)
{
let thread = audio.start_encoder(&name, track, shutdown_token)?;
self.active_audio.insert(name, thread);
Ok(())
} else {
info!("ignoring track request {name}: rendition not available");
Err(n0_error::anyerr!("rendition not available"))
}
}
}
pub struct AudioRenditions {
make_encoder: Box<dyn Fn(AudioPreset) -> Result<Box<dyn AudioEncoder>> + Send>,
source: Box<dyn AudioSource>,
renditions: HashMap<String, AudioPreset>,
}
impl AudioRenditions {
pub fn new<E: AudioEncoder>(
source: impl AudioSource,
presets: impl IntoIterator<Item = AudioPreset>,
) -> Self {
let renditions = presets
.into_iter()
.map(|preset| (format!("audio-{preset}"), preset))
.collect();
let format = source.format();
Self {
make_encoder: Box::new(move |preset| Ok(Box::new(E::with_preset(format, preset)?))),
renditions,
source: Box::new(source),
}
}
pub fn available_renditions(&self) -> Result<BTreeMap<String, AudioConfig>> {
let mut renditions = BTreeMap::new();
for (name, preset) in self.renditions.iter() {
// We need to create the encoder to get the config, even though we drop it
// again (it will be created on deman). Not ideal, but works for now.
let config = (self.make_encoder)(*preset)?.config();
renditions.insert(name.clone(), config);
}
Ok(renditions)
}
pub fn encoder(&mut self, name: &str) -> Option<Result<Box<dyn AudioEncoder>>> {
let preset = self.renditions.get(name)?;
Some((self.make_encoder)(*preset))
}
pub fn contains_rendition(&self, name: &str) -> bool {
self.renditions.contains_key(name)
}
pub fn start_encoder(
&mut self,
name: &str,
producer: hang::TrackProducer,
shutdown_token: CancellationToken,
) -> Result<EncoderThread> {
let preset = self
.renditions
.get(name)
.context("rendition not available")?;
let encoder = (self.make_encoder)(*preset)?;
let thread = EncoderThread::spawn_audio(
self.source.cloned_boxed(),
encoder,
producer,
shutdown_token,
);
Ok(thread)
}
}
pub struct VideoRenditions {
make_encoder: Box<dyn Fn(VideoPreset) -> Result<Box<dyn VideoEncoder>> + Send>,
source: SharedVideoSource,
renditions: HashMap<String, VideoPreset>,
_shared_source_cancel_guard: DropGuard,
}
impl VideoRenditions {
pub fn new<E: VideoEncoder>(
source: impl VideoSource,
presets: impl IntoIterator<Item = VideoPreset>,
) -> Self {
let shutdown_token = CancellationToken::new();
let source = SharedVideoSource::new(source, shutdown_token.clone());
let renditions = presets
.into_iter()
.map(|preset| (format!("video-{preset}"), preset))
.collect();
Self {
make_encoder: Box::new(|preset| Ok(Box::new(E::with_preset(preset)?))),
renditions,
source,
_shared_source_cancel_guard: shutdown_token.drop_guard(),
}
}
pub fn available_renditions(&self) -> Result<BTreeMap<String, VideoConfig>> {
let mut renditions = BTreeMap::new();
for (name, preset) in self.renditions.iter() {
// We need to create the encoder to get the config, even though we drop it
// again (it will be created on deman). Not ideal, but works for now.
let config = (self.make_encoder)(*preset)?.config();
renditions.insert(name.clone(), config);
}
Ok(renditions)
}
pub fn contains_rendition(&self, name: &str) -> bool {
self.renditions.contains_key(name)
}
pub fn start_encoder(
&mut self,
name: &str,
producer: hang::TrackProducer,
shutdown_token: CancellationToken,
) -> Result<EncoderThread> {
let preset = self
.renditions
.get(name)
.context("rendition not available")?;
let encoder = (self.make_encoder)(*preset)?;
let thread =
EncoderThread::spawn_video(self.source.clone(), encoder, producer, shutdown_token);
Ok(thread)
}
}
#[derive(Debug, Clone)]
pub(crate) struct SharedVideoSource {
name: String,
frames_rx: tokio::sync::watch::Receiver<Option<crate::av::VideoFrame>>,
format: crate::av::VideoFormat,
running: Arc<AtomicBool>,
thread: Arc<std::thread::JoinHandle<()>>,
subscriber_count: Arc<AtomicU32>,
}
impl SharedVideoSource {
fn new(mut source: impl VideoSource, shutdown: CancellationToken) -> Self {
let name = source.name().to_string();
let format = source.format();
let (tx, rx) = tokio::sync::watch::channel(None);
let running = Arc::new(AtomicBool::new(false));
let thread = spawn_thread(format!("vshr-{}", source.name()), {
let shutdown = shutdown.clone();
let running = running.clone();
move || {
let frame_time = Duration::from_secs_f32(1. / 30.);
let start = Instant::now();
for i in 0.. {
if shutdown.is_cancelled() {
break;
}
loop {
if running.load(Ordering::Relaxed) {
break;
}
if let Err(err) = source.stop() {
warn!("Failed to stop video source: {err:#}");
}
std::thread::park();
if let Err(err) = source.start() {
warn!("Failed to stop video source: {err:#}");
}
}
match source.pop_frame() {
Ok(Some(frame)) => {
let _ = tx.send(Some(frame));
}
Ok(None) => {}
Err(_) => break,
}
let expected = frame_time * i;
let actual = start.elapsed();
if actual < expected {
std::thread::sleep(expected - actual);
}
}
}
});
Self {
name,
format,
frames_rx: rx,
thread: Arc::new(thread),
running,
subscriber_count: Default::default(),
}
}
}
impl VideoSource for SharedVideoSource {
fn name(&self) -> &str {
&self.name
}
fn format(&self) -> crate::av::VideoFormat {
self.format.clone()
}
fn start(&mut self) -> anyhow::Result<()> {
let prev_count = self.subscriber_count.fetch_add(1, Ordering::Relaxed);
if prev_count == 0 {
self.running.store(true, Ordering::Relaxed);
self.thread.thread().unpark();
}
Ok(())
}
fn stop(&mut self) -> anyhow::Result<()> {
if self
.subscriber_count
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |val| {
Some(val.saturating_sub(1))
})
.expect("always returns Some")
== 1
{
self.running.store(false, Ordering::Relaxed);
}
Ok(())
}
fn pop_frame(&mut self) -> anyhow::Result<Option<crate::av::VideoFrame>> {
let frame = self.frames_rx.borrow_and_update().clone();
Ok(frame)
}
}
pub struct EncoderThread {
_thread_handle: std::thread::JoinHandle<()>,
shutdown: CancellationToken,
}
impl EncoderThread {
pub fn spawn_video(
mut source: impl VideoSource,
mut encoder: impl VideoEncoderInner,
mut producer: hang::TrackProducer,
shutdown: CancellationToken,
) -> Self {
let thread_name = format!("venc-{:<4}-{:<4}", source.name(), encoder.name());
let span = info_span!("videoenc", source = source.name(), encoder = encoder.name());
let handle = spawn_thread(thread_name, {
let shutdown = shutdown.clone();
move || {
let _guard = span.enter();
if let Err(err) = source.start() {
warn!("video source failed to start: {err:#}");
return;
}
let format = source.format();
tracing::debug!(
src_format = ?format,
dst_config = ?encoder.config(),
"video encoder thread start"
);
let framerate = encoder.config().framerate.unwrap_or(30.0);
let interval = Duration::from_secs_f64(1. / framerate);
loop {
let start = Instant::now();
if shutdown.is_cancelled() {
debug!("stop video encoder: cancelled");
break;
}
let frame = match source.pop_frame() {
Ok(frame) => frame,
Err(err) => {
warn!("video encoder failed: {err:#}");
break;
}
};
if let Some(frame) = frame {
if let Err(err) = encoder.push_frame(frame) {
warn!("video encoder failed: {err:#}");
break;
};
while let Ok(Some(pkt)) = encoder.pop_packet() {
if let Err(err) = producer.write(pkt) {
warn!("failed to write frame to producer: {err:#}");
}
}
}
std::thread::sleep(interval.saturating_sub(start.elapsed()));
}
producer.inner.close();
if let Err(err) = source.stop() {
warn!("video source failed to stop: {err:#}");
}
tracing::debug!("video encoder thread stop");
}
});
Self {
_thread_handle: handle,
shutdown,
}
}
pub fn spawn_audio(
mut source: Box<dyn AudioSource>,
mut encoder: impl AudioEncoderInner,
mut producer: hang::TrackProducer,
shutdown: CancellationToken,
) -> Self {
let sd = shutdown.clone();
let name = encoder.name();
let thread_name = format!("aenc-{:<4}", name);
let span = info_span!("audioenc", %name);
let handle = spawn_thread(thread_name, move || {
let _guard = span.enter();
tracing::debug!(config=?encoder.config(), "audio encoder thread start");
let shutdown = sd;
// 20ms framing to align with typical Opus config (48kHz → 960 samples/ch)
const INTERVAL: Duration = Duration::from_millis(20);
let format = source.format();
let samples_per_frame = (format.sample_rate / 1000) * INTERVAL.as_millis() as u32;
let mut buf = vec![0.0f32; samples_per_frame as usize * format.channel_count as usize];
let start = Instant::now();
for tick in 0.. {
trace!("tick");
if shutdown.is_cancelled() {
break;
}
match source.pop_samples(&mut buf) {
Ok(Some(_n)) => {
// Expect a full frame; if shorter, zero-pad via slice len
if let Err(err) = encoder.push_samples(&buf) {
error!(buf_len = buf.len(), "audio push_samples failed: {err:#}");
break;
}
while let Ok(Some(pkt)) = encoder
.pop_packet()
.inspect_err(|err| warn!("encoder error: {err:#}"))
{
if let Err(err) = producer.write(pkt) {
warn!("failed to write frame to producer: {err:#}");
}
}
}
Ok(None) => {
// keep pacing
}
Err(err) => {
error!("audio source failed: {err:#}");
break;
}
}
let expected_time = (tick + 1) * INTERVAL;
let actual_time = start.elapsed();
if actual_time > expected_time {
warn!("audio thread too slow by {:?}", actual_time - expected_time);
}
let sleep = expected_time.saturating_sub(start.elapsed());
if sleep > Duration::ZERO {
std::thread::sleep(sleep);
}
}
// drain
while let Ok(Some(pkt)) = encoder.pop_packet() {
if let Err(err) = producer.write(pkt) {
warn!("failed to write frame to producer: {err:#}");
}
}
producer.inner.close();
tracing::debug!("audio encoder thread stop");
});
Self {
_thread_handle: handle,
shutdown,
}
}
}
impl Drop for EncoderThread {
fn drop(&mut self) {
self.shutdown.cancel();
}
}

View file

@ -0,0 +1,712 @@
use std::{collections::BTreeMap, sync::Arc, time::Duration};
use hang::{
Timestamp, TrackConsumer,
catalog::{AudioConfig, Catalog, CatalogConsumer, VideoConfig},
};
use moq_lite::{BroadcastConsumer, Track};
use n0_error::{Result, StackResultExt, StdResultExt};
use n0_future::task::AbortOnDropHandle;
use n0_watcher::{Watchable, Watcher};
use tokio::{
sync::mpsc::{self, error::TryRecvError},
time::Instant,
};
use tokio_util::sync::{CancellationToken, DropGuard};
use tracing::{Span, debug, error, info, info_span, trace, warn};
use crate::{
av::{
AudioDecoder, AudioSink, AudioSinkHandle, DecodeConfig, DecodedFrame, Decoders,
PlaybackConfig, Quality, VideoDecoder, VideoSource,
},
ffmpeg::util::Rescaler,
util::spawn_thread,
};
const DEFAULT_MAX_LATENCY: Duration = Duration::from_millis(150);
#[derive(derive_more::Debug, Clone)]
pub struct SubscribeBroadcast {
broadcast_name: String,
#[debug("BroadcastConsumer")]
broadcast: BroadcastConsumer,
// catalog_watcher: n0_watcher::Direct<CatalogWrapper>,
catalog_watchable: Watchable<CatalogWrapper>,
shutdown: CancellationToken,
_catalog_task: Arc<AbortOnDropHandle<()>>,
}
#[derive(Debug, derive_more::PartialEq, derive_more::Eq, Default, Clone, derive_more::Deref)]
pub struct CatalogWrapper {
#[eq(skip)]
#[deref]
inner: Arc<Catalog>,
seq: usize,
}
impl CatalogWrapper {
fn new(inner: Catalog, seq: usize) -> Self {
Self {
inner: Arc::new(inner),
seq,
}
}
pub fn video_renditions(&self) -> impl Iterator<Item = &str> {
let mut renditions: Vec<_> = self
.inner
.video
.as_ref()
.iter()
.map(|v| v.renditions.iter())
.flatten()
.map(|(name, config)| (name.as_str(), config.coded_width))
.collect();
renditions.sort_by(|a, b| a.1.cmp(&b.1));
renditions.into_iter().map(|(name, _w)| name)
}
pub fn audio_renditions(&self) -> impl Iterator<Item = &str> + '_ {
self.inner
.audio
.as_ref()
.into_iter()
.map(|v| v.renditions.iter())
.flatten()
.map(|(name, _config)| name.as_str())
}
pub fn select_video_rendition(&self, quality: Quality) -> Result<String> {
let video = self.inner.video.as_ref().context("no video published")?;
let track_name =
select_video_rendition(&video.renditions, quality).context("no video renditions")?;
Ok(track_name)
}
pub fn select_audio_rendition(&self, quality: Quality) -> Result<String> {
let audio = self.inner.audio.as_ref().context("no video published")?;
let track_name =
select_audio_rendition(&audio.renditions, quality).context("no video renditions")?;
Ok(track_name)
}
}
impl CatalogWrapper {
pub fn into_inner(self) -> Arc<Catalog> {
self.inner
}
}
impl SubscribeBroadcast {
pub async fn new(broadcast_name: String, broadcast: BroadcastConsumer) -> Result<Self> {
let shutdown = CancellationToken::new();
let (catalog_watchable, catalog_task) = {
let track = broadcast.subscribe_track(&Catalog::default_track());
let mut consumer = CatalogConsumer::new(track);
let initial_catalog = consumer
.next()
.await
.std_context("Broadcast closed before receiving catalog")?
.context("Catalog track closed before receiving catalog")?;
let watchable = Watchable::new(CatalogWrapper::new(initial_catalog, 0));
let task = tokio::spawn({
let shutdown = shutdown.clone();
let watchable = watchable.clone();
async move {
for seq in 1.. {
match consumer.next().await {
Ok(Some(catalog)) => {
watchable.set(CatalogWrapper::new(catalog, seq)).ok();
}
Ok(None) => {
debug!("subscribed broadcast catalog track ended");
break;
}
Err(err) => {
debug!("subscribed broadcast closed: {err:#}");
break;
}
}
}
shutdown.cancel();
}
});
(watchable, task)
};
Ok(Self {
broadcast_name,
broadcast,
catalog_watchable,
_catalog_task: Arc::new(AbortOnDropHandle::new(catalog_task)),
shutdown: CancellationToken::new(),
})
}
pub fn broadcast_name(&self) -> &str {
&self.broadcast_name
}
pub fn catalog_watcher(&mut self) -> n0_watcher::Direct<CatalogWrapper> {
self.catalog_watchable.watch()
}
pub fn catalog(&self) -> CatalogWrapper {
self.catalog_watchable.get()
}
pub fn watch_and_listen<D: Decoders>(
self,
audio_out: impl AudioSink,
playback_config: PlaybackConfig,
) -> Result<AvRemoteTrack> {
AvRemoteTrack::new::<D>(self, audio_out, playback_config)
}
pub fn watch<D: VideoDecoder>(&self) -> Result<WatchTrack> {
self.watch_with::<D>(&Default::default(), Quality::Highest)
}
pub fn watch_with<D: VideoDecoder>(
&self,
playback_config: &DecodeConfig,
quality: Quality,
) -> Result<WatchTrack> {
let track_name = self.catalog().select_video_rendition(quality)?;
self.watch_rendition::<D>(playback_config, &track_name)
}
pub fn watch_rendition<D: VideoDecoder>(
&self,
playback_config: &DecodeConfig,
track_name: &str,
) -> Result<WatchTrack> {
let catalog = self.catalog();
let video = catalog.video.as_ref().context("no video published")?;
let config = video
.renditions
.get(track_name)
.context("rendition not found")?;
let consumer = TrackConsumer::new(
self.broadcast.subscribe_track(&Track {
name: track_name.to_string(),
priority: video.priority,
}),
DEFAULT_MAX_LATENCY,
);
let span = info_span!("videodec", %track_name);
WatchTrack::from_consumer::<D>(
track_name.to_string(),
consumer,
&config,
playback_config,
self.shutdown.child_token(),
span,
)
}
pub fn listen<D: AudioDecoder>(&self, output: impl AudioSink) -> Result<AudioTrack> {
self.listen_with::<D>(Quality::Highest, output)
}
pub fn listen_with<D: AudioDecoder>(
&self,
quality: Quality,
output: impl AudioSink,
) -> Result<AudioTrack> {
let track_name = self.catalog().select_audio_rendition(quality)?;
self.listen_rendition::<D>(&track_name, output)
}
pub fn listen_rendition<D: AudioDecoder>(
&self,
name: &str,
output: impl AudioSink,
) -> Result<AudioTrack> {
let catalog = self.catalog();
let audio = catalog.audio.as_ref().context("no audio published")?;
let config = audio.renditions.get(name).context("rendition not found")?;
let consumer = TrackConsumer::new(
self.broadcast.subscribe_track(&Track {
name: name.to_string(),
priority: audio.priority,
}),
DEFAULT_MAX_LATENCY,
);
let span = info_span!("audiodec", %name);
AudioTrack::spawn::<D>(
name.to_string(),
consumer,
config.clone(),
output,
self.shutdown.child_token(),
span,
)
}
pub fn closed(&self) -> impl Future<Output = ()> + 'static {
self.broadcast.closed()
}
pub fn shutdown(&self) {
self.shutdown.cancel();
}
}
fn select_rendition<T, P: ToString>(
renditions: &BTreeMap<String, T>,
order: &[P],
) -> Option<String> {
order
.iter()
.map(ToString::to_string)
.find(|k| renditions.contains_key(k.as_str()))
.or_else(|| renditions.keys().next().cloned())
}
fn select_video_rendition<'a, T>(
renditions: &'a BTreeMap<String, T>,
q: Quality,
) -> Option<String> {
use crate::av::VideoPreset::*;
let order = match q {
Quality::Highest => [P1080, P720, P360, P180],
Quality::High => [P720, P360, P180, P1080],
Quality::Mid => [P360, P180, P720, P1080],
Quality::Low => [P180, P360, P720, P1080],
};
select_rendition(renditions, &order)
}
fn select_audio_rendition<'a, T>(
renditions: &'a BTreeMap<String, T>,
q: Quality,
) -> Option<String> {
use crate::av::AudioPreset::*;
let order = match q {
Quality::Highest | Quality::High => [Hq, Lq],
Quality::Mid | Quality::Low => [Lq, Hq],
};
select_rendition(renditions, &order)
}
pub struct AudioTrack {
name: String,
handle: Box<dyn AudioSinkHandle>,
shutdown_token: CancellationToken,
_task_handle: AbortOnDropHandle<()>,
_thread_handle: std::thread::JoinHandle<()>,
}
impl AudioTrack {
pub(crate) fn spawn<D: AudioDecoder>(
name: String,
consumer: TrackConsumer,
config: AudioConfig,
output: impl AudioSink,
shutdown: CancellationToken,
span: Span,
) -> Result<Self> {
let _guard = span.enter();
let (packet_tx, packet_rx) = mpsc::channel(32);
let output_format = output.format()?;
info!(?config, "audio thread start");
let decoder = D::new(&config, output_format)?;
let handle = output.handle();
let thread_name = format!("adec-{}", name);
let thread = spawn_thread(thread_name, {
let shutdown = shutdown.clone();
let span = span.clone();
move || {
let _guard = span.enter();
if let Err(err) = Self::run_loop(decoder, packet_rx, output, &shutdown) {
error!("audio decoder failed: {err:#}");
}
info!("audio decoder thread stop");
}
});
let task = tokio::spawn(forward_frames(consumer, packet_tx));
Ok(Self {
name,
handle,
shutdown_token: shutdown,
_task_handle: AbortOnDropHandle::new(task),
_thread_handle: thread,
})
}
pub fn stopped(&self) -> impl Future<Output = ()> + 'static {
let shutdown_token = self.shutdown_token.clone();
async move { shutdown_token.cancelled().await }
}
pub fn rendition(&self) -> &str {
&self.name
}
pub fn handle(&self) -> &dyn AudioSinkHandle {
self.handle.as_ref()
}
pub(crate) fn run_loop(
mut decoder: impl AudioDecoder,
mut packet_rx: mpsc::Receiver<hang::Frame>,
mut sink: impl AudioSink,
shutdown: &CancellationToken,
) -> Result<()> {
const INTERVAL: Duration = Duration::from_millis(10);
let mut remote_start = None;
let loop_start = Instant::now();
'main: for i in 0.. {
let tick = Instant::now();
if shutdown.is_cancelled() {
debug!("stop audio thread: cancelled");
break;
}
loop {
match packet_rx.try_recv() {
Ok(packet) => {
let remote_start = *remote_start.get_or_insert_with(|| packet.timestamp);
if tracing::enabled!(tracing::Level::TRACE) {
let loop_elapsed = tick.duration_since(loop_start);
let remote_elapsed: Duration = packet
.timestamp
.checked_sub(remote_start)
.unwrap_or(Timestamp::ZERO)
.into();
let diff_ms =
(loop_elapsed.as_secs_f32() - remote_elapsed.as_secs_f32()) * 1000.;
trace!(len = packet.payload.num_bytes(), ts=?packet.timestamp, ?loop_elapsed, ?remote_elapsed, ?diff_ms, "recv packet");
}
// TODO: Skip outdated packets?
if !sink.is_paused() {
decoder.push_packet(packet)?;
if let Some(samples) = decoder.pop_samples()? {
sink.push_samples(samples)?;
}
}
}
Err(TryRecvError::Disconnected) => {
debug!("stop audio thread: packet_rx disconnected");
break 'main;
}
Err(TryRecvError::Empty) => {
trace!("no packet to recv");
break;
}
}
}
let target_time = i * INTERVAL;
let real_time = Instant::now().duration_since(loop_start);
let sleep = target_time.saturating_sub(real_time);
if !sleep.is_zero() {
std::thread::sleep(sleep);
}
}
shutdown.cancel();
Ok(())
}
}
impl Drop for AudioTrack {
fn drop(&mut self) {
self.shutdown_token.cancel();
}
}
pub struct WatchTrack {
video_frames: WatchTrackFrames,
handle: WatchTrackHandle,
}
pub struct WatchTrackHandle {
rendition: String,
viewport: Watchable<(u32, u32)>,
_guard: WatchTrackGuard,
}
impl WatchTrackHandle {
pub fn set_viewport(&self, w: u32, h: u32) {
self.viewport.set((w, h)).ok();
}
pub fn rendition(&self) -> &str {
&self.rendition
}
}
pub struct WatchTrackFrames {
rx: mpsc::Receiver<DecodedFrame>,
}
impl WatchTrackFrames {
pub fn current_frame(&mut self) -> Option<DecodedFrame> {
let mut out = None;
while let Ok(item) = self.rx.try_recv() {
out = Some(item);
}
out
}
pub async fn next_frame(&mut self) -> Option<DecodedFrame> {
if let Some(frame) = self.current_frame() {
Some(frame)
} else {
self.rx.recv().await
}
}
}
struct WatchTrackGuard {
_shutdown_token_guard: DropGuard,
_task_handle: Option<AbortOnDropHandle<()>>,
_thread_handle: Option<std::thread::JoinHandle<()>>,
}
impl WatchTrack {
pub fn empty(rendition: impl ToString) -> Self {
let (tx, rx) = mpsc::channel(1);
let task = tokio::task::spawn(async move {
std::future::pending::<()>().await;
let _ = tx;
});
let guard = WatchTrackGuard {
_shutdown_token_guard: CancellationToken::new().drop_guard(),
_task_handle: Some(AbortOnDropHandle::new(task)),
_thread_handle: None,
};
Self {
video_frames: WatchTrackFrames { rx },
handle: WatchTrackHandle {
rendition: rendition.to_string(),
viewport: Default::default(),
_guard: guard,
},
}
}
pub fn from_video_source(
rendition: String,
shutdown: CancellationToken,
mut source: impl VideoSource,
decode_config: DecodeConfig,
) -> Self {
let viewport = Watchable::new((1u32, 1u32));
let (frame_tx, frame_rx) = tokio::sync::mpsc::channel::<DecodedFrame>(2);
let thread_name = format!("vpr-{:>4}-{:>4}", source.name(), rendition);
let thread = spawn_thread(thread_name, {
let mut viewport = viewport.watch();
let shutdown = shutdown.clone();
move || {
// TODO: Make configurable.
let fps = 30;
let mut rescaler = Rescaler::new(decode_config.pixel_format.to_ffmpeg(), None)
.expect("failed to create rescaler");
let frame_duration = Duration::from_secs_f32(1. / fps as f32);
if let Err(err) = source.start() {
warn!("Video source failed to start: {err:?}");
return;
}
let start = Instant::now();
for i in 1.. {
// let t = Instant::now();
if shutdown.is_cancelled() {
break;
}
if viewport.update() {
let (w, h) = viewport.peek();
rescaler.set_target_dimensions(*w, *h);
}
match source.pop_frame() {
Ok(Some(frame)) => {
// trace!(t=?t.elapsed(), "pop");
let frame = frame.to_ffmpeg();
let frame = rescaler.process(&frame).expect("rescaler failed");
let frame =
DecodedFrame::from_ffmpeg(frame, frame_duration, start.elapsed());
// trace!(t=?t.elapsed(), "convert");
let _ = frame_tx.blocking_send(frame);
// trace!(t=?t.elapsed(), "send");
}
Ok(None) => {}
Err(_) => break,
}
let expected_time = i * frame_duration;
let actual_time = start.elapsed();
if expected_time > actual_time {
std::thread::sleep(expected_time - actual_time);
// trace!(t=?t.elapsed(), slept=?(actual_time - expected_time), ?expected_time, ?actual_time, "done");
}
}
if let Err(err) = source.stop() {
warn!("Video source failed to stop: {err:?}");
return;
}
}
});
let guard = WatchTrackGuard {
_shutdown_token_guard: shutdown.drop_guard(),
_task_handle: None,
_thread_handle: Some(thread),
};
WatchTrack {
video_frames: WatchTrackFrames { rx: frame_rx },
handle: WatchTrackHandle {
rendition,
viewport,
_guard: guard,
},
}
}
pub(crate) fn from_consumer<D: VideoDecoder>(
rendition: String,
consumer: TrackConsumer,
config: &VideoConfig,
playback_config: &DecodeConfig,
shutdown: CancellationToken,
span: Span,
) -> Result<Self> {
let (packet_tx, packet_rx) = mpsc::channel(32);
let (frame_tx, frame_rx) = mpsc::channel(32);
let viewport = Watchable::new((1u32, 1u32));
let viewport_watcher = viewport.watch();
let _guard = span.enter();
debug!(?config, "video decoder start");
let decoder = D::new(config, playback_config)?;
let thread_name = format!("vdec-{}", rendition);
let thread = spawn_thread(thread_name, {
let shutdown = shutdown.clone();
let span = span.clone();
move || {
let _guard = span.enter();
if let Err(err) =
Self::run_loop(&shutdown, packet_rx, frame_tx, viewport_watcher, decoder)
{
error!("video decoder failed: {err:#}");
}
shutdown.cancel();
}
});
let task = tokio::task::spawn(forward_frames(consumer, packet_tx));
let guard = WatchTrackGuard {
_shutdown_token_guard: shutdown.drop_guard(),
_task_handle: Some(AbortOnDropHandle::new(task)),
_thread_handle: Some(thread),
};
Ok(WatchTrack {
video_frames: WatchTrackFrames { rx: frame_rx },
handle: WatchTrackHandle {
rendition,
viewport,
_guard: guard,
},
})
}
pub fn split(self) -> (WatchTrackFrames, WatchTrackHandle) {
(self.video_frames, self.handle)
}
pub fn set_viewport(&self, w: u32, h: u32) {
self.handle.set_viewport(w, h);
}
pub fn rendition(&self) -> &str {
self.handle.rendition()
}
pub fn current_frame(&mut self) -> Option<DecodedFrame> {
self.video_frames.current_frame()
}
pub(crate) fn run_loop(
shutdown: &CancellationToken,
mut input_rx: mpsc::Receiver<hang::Frame>,
output_tx: mpsc::Sender<DecodedFrame>,
mut viewport_watcher: n0_watcher::Direct<(u32, u32)>,
mut decoder: impl VideoDecoder,
) -> Result<(), anyhow::Error> {
loop {
if shutdown.is_cancelled() {
break;
}
let Some(packet) = input_rx.blocking_recv() else {
break;
};
if viewport_watcher.update() {
let (w, h) = viewport_watcher.peek();
decoder.set_viewport(*w, *h);
}
let t = Instant::now();
decoder
.push_packet(packet)
.context("failed to push packet")?;
trace!(t=?t.elapsed(), "videodec: push_packet");
while let Some(frame) = decoder.pop_frame().context("failed to pop frame")? {
trace!(t=?t.elapsed(), "videodec: pop frame");
if output_tx.blocking_send(frame).is_err() {
break;
}
trace!(t=?t.elapsed(), "videodec: tx");
}
}
Ok(())
}
}
async fn forward_frames(mut track: hang::TrackConsumer, sender: mpsc::Sender<hang::Frame>) {
loop {
let frame = track.read_frame().await;
match frame {
Ok(Some(frame)) => {
if sender.send(frame).await.is_err() {
break;
}
}
Ok(None) => break,
Err(err) => {
warn!("failed to read frame: {err:?}");
break;
}
}
}
}
pub struct AvRemoteTrack {
pub broadcast: SubscribeBroadcast,
pub video: Option<WatchTrack>,
pub audio: Option<AudioTrack>,
}
impl AvRemoteTrack {
pub fn new<D: Decoders>(
broadcast: SubscribeBroadcast,
audio_out: impl AudioSink,
playback_config: PlaybackConfig,
) -> Result<Self> {
let audio = broadcast
.listen_with::<D::Audio>(playback_config.quality, audio_out)
.inspect_err(|err| tracing::warn!("no audio track: {err}"))
.ok();
let video = broadcast
.watch_with::<D::Video>(&playback_config.decode_config, playback_config.quality)
.inspect_err(|err| tracing::warn!("no video track: {err}"))
.ok();
Ok(Self {
broadcast,
audio,
video,
})
}
}

View file

@ -0,0 +1,12 @@
/// Spawn a named OS thread and panic if spawning fails.
pub fn spawn_thread<F, T>(name: impl ToString, f: F) -> std::thread::JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let name_str = name.to_string();
std::thread::Builder::new()
.name(name_str.clone())
.spawn(f)
.expect(&format!("failed to spawn thread: {}", name_str))
}