#[cfg(not(have_libwebrtc))]
fn main() {
eprintln!(
"this example needs a native libwebrtc — set REACTOR_WEBRTC_LIB_DIR \
(a webrtc-build/out/<target>/dist dir) or REACTOR_WEBRTC_PREBUILT_URL \
and rebuild."
);
}
#[cfg(have_libwebrtc)]
fn main() {
imp::run();
}
#[cfg(have_libwebrtc)]
mod imp {
use std::collections::VecDeque;
use std::fs::{self, File};
use std::io::{Seek, SeekFrom, Write};
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use reactor_webrtc::{
MediaKind, PeerConnection, PeerConnectionFactory, PeerConnectionObserver,
PeerConnectionState, RtcConfiguration, Track,
};
const W: usize = 320;
const H: usize = 240;
const RATE: u32 = 48_000;
const CHANNELS: u32 = 2;
pub fn run() {
let mut args = std::env::args().skip(1);
let out_dir = args.next().unwrap_or_else(|| "loopback-out".into());
let seconds: u64 = args.next().and_then(|s| s.parse().ok()).unwrap_or(3);
fs::create_dir_all(&out_dir).expect("create out dir");
let y4m_path = Path::new(&out_dir).join("loopback.y4m");
let wav_path = Path::new(&out_dir).join("loopback.wav");
let video_writer = Arc::new(Mutex::new(Y4mWriter::new(&y4m_path)));
let wav_writer = Arc::new(Mutex::new(WavWriter::new(&wav_path)));
let frames = Arc::new(AtomicU32::new(0));
let blocks = Arc::new(AtomicU32::new(0));
let factory = PeerConnectionFactory::new().expect("factory");
let config = RtcConfiguration::default();
let recv_tracks: Arc<Mutex<Vec<Track>>> = Arc::new(Mutex::new(Vec::new()));
let (recv_pc, recv_state) = make_peer(&factory, &config, {
let vw = video_writer.clone();
let aw = wav_writer.clone();
let frames = frames.clone();
let blocks = blocks.clone();
let recv_tracks = recv_tracks.clone();
move |kind, mut track| {
match kind {
MediaKind::Video => {
let vw = vw.clone();
let frames = frames.clone();
track.on_video_frame(move |f| {
vw.lock()
.unwrap()
.write(f.bgra, f.width as usize, f.height as usize);
frames.fetch_add(1, Ordering::SeqCst);
});
}
MediaKind::Audio => {
let aw = aw.clone();
let blocks = blocks.clone();
track.on_audio_frame(move |f| {
aw.lock().unwrap().write(f.pcm, f.sample_rate, f.channels);
blocks.fetch_add(1, Ordering::SeqCst);
});
}
MediaKind::Unknown => {}
}
recv_tracks.lock().unwrap().push(track);
}
});
let (send_pc, send_state) = make_peer(&factory, &config, |_, _| {});
let video = factory.create_video_track("video").expect("video track");
let audio = factory.create_audio_track("audio").expect("audio track");
send_pc.add_track(&video).expect("add video");
send_pc.add_track(&audio).expect("add audio");
let offer = send_pc.create_offer().expect("offer");
send_pc
.set_local_description(&offer)
.expect("send local offer");
recv_pc
.set_remote_description(&offer)
.expect("recv remote offer");
let answer = recv_pc.create_answer().expect("answer");
recv_pc
.set_local_description(&answer)
.expect("recv local answer");
send_pc
.set_remote_description(&answer)
.expect("send remote answer");
println!("negotiated; capturing ~{seconds}s to {out_dir}/ …");
let stop = AtomicBool::new(false);
thread::scope(|scope| {
scope.spawn(|| {
while !stop.load(Ordering::SeqCst) {
forward_ice(&send_state, &recv_pc);
forward_ice(&recv_state, &send_pc);
thread::sleep(Duration::from_millis(20));
}
});
scope.spawn(|| {
let spc = (RATE / 100) as usize; let mut bgra = vec![0u8; W * H * 4];
let mut pcm = vec![0i16; spc * CHANNELS as usize];
let mut phase = 0.0f32;
let mut tick = 0u32;
while !stop.load(Ordering::SeqCst) {
fill_tone(&mut pcm, &mut phase, CHANNELS as usize);
factory.push_audio_frame(&pcm, RATE, CHANNELS);
if tick % 3 == 0 {
fill_pattern(&mut bgra, tick);
video.push_video_frame(&bgra, W as u32, H as u32);
}
tick = tick.wrapping_add(1);
thread::sleep(Duration::from_millis(10));
}
});
let start = Instant::now();
while !(send_state.connected.load(Ordering::SeqCst)
&& recv_state.connected.load(Ordering::SeqCst))
&& start.elapsed() < Duration::from_secs(10)
{
thread::sleep(Duration::from_millis(50));
}
thread::sleep(Duration::from_secs(seconds));
stop.store(true, Ordering::SeqCst);
});
wav_writer.lock().unwrap().finalize();
println!(
"done — wrote {} video frames → {} and {} audio blocks → {}",
frames.load(Ordering::SeqCst),
y4m_path.display(),
blocks.load(Ordering::SeqCst),
wav_path.display(),
);
}
struct State {
ice: Mutex<VecDeque<reactor_webrtc::IceCandidate>>,
connected: AtomicBool,
}
fn make_peer(
factory: &PeerConnectionFactory,
config: &RtcConfiguration,
on_track: impl FnMut(MediaKind, Track) + Send + 'static,
) -> (PeerConnection, Arc<State>) {
let state = Arc::new(State {
ice: Mutex::new(VecDeque::new()),
connected: AtomicBool::new(false),
});
let observer = PeerConnectionObserver::new()
.on_ice_candidate({
let s = state.clone();
move |c| s.ice.lock().unwrap().push_back(c)
})
.on_connection_state_change({
let s = state.clone();
move |st| {
if st == PeerConnectionState::Connected {
s.connected.store(true, Ordering::SeqCst);
}
}
})
.on_track(on_track);
let pc = factory
.create_peer_connection(config, observer)
.expect("create peer connection");
(pc, state)
}
fn forward_ice(from: &State, to: &PeerConnection) {
while let Some(c) = {
let mut q = from.ice.lock().unwrap();
q.pop_front()
} {
let _ = to.add_ice_candidate(&c);
}
}
fn fill_pattern(bgra: &mut [u8], tick: u32) {
for y in 0..H {
for x in 0..W {
let p = (y * W + x) * 4;
bgra[p] = (x + tick as usize) as u8; bgra[p + 1] = (y + tick as usize) as u8; bgra[p + 2] = (x + y) as u8; bgra[p + 3] = 0xff; }
}
}
fn fill_tone(pcm: &mut [i16], phase: &mut f32, channels: usize) {
let step = 2.0 * std::f32::consts::PI * 440.0 / RATE as f32;
for frame in pcm.chunks_mut(channels) {
let s = (phase.sin() * 8000.0) as i16;
for c in frame.iter_mut() {
*c = s;
}
*phase += step;
if *phase > 2.0 * std::f32::consts::PI {
*phase -= 2.0 * std::f32::consts::PI;
}
}
}
struct Y4mWriter {
file: File,
header: bool,
}
impl Y4mWriter {
fn new(path: &Path) -> Self {
Self {
file: File::create(path).expect("create y4m"),
header: false,
}
}
fn write(&mut self, bgra: &[u8], w: usize, h: usize) {
if w == 0 || h == 0 || bgra.len() < w * h * 4 {
return;
}
if !self.header {
let _ = writeln!(self.file, "YUV4MPEG2 W{w} H{h} F30:1 Ip A1:1 C420");
self.header = true;
}
let (cw, ch) = (w / 2, h / 2);
let mut plane = Vec::with_capacity(w * h + 2 * cw * ch);
for j in 0..h {
for i in 0..w {
let p = (j * w + i) * 4;
let (b, g, r) = (bgra[p] as f32, bgra[p + 1] as f32, bgra[p + 2] as f32);
plane.push(clamp8(0.257 * r + 0.504 * g + 0.098 * b + 16.0));
}
}
for &(cu, cv) in &[(true, false), (false, true)] {
for j in (0..h).step_by(2) {
for i in (0..w).step_by(2) {
let p = (j * w + i) * 4;
let (b, g, r) = (bgra[p] as f32, bgra[p + 1] as f32, bgra[p + 2] as f32);
let val = if cu {
-0.148 * r - 0.291 * g + 0.439 * b + 128.0
} else if cv {
0.439 * r - 0.368 * g - 0.071 * b + 128.0
} else {
128.0
};
plane.push(clamp8(val));
}
}
}
let _ = self.file.write_all(b"FRAME\n");
let _ = self.file.write_all(&plane);
}
}
fn clamp8(v: f32) -> u8 {
v.clamp(0.0, 255.0) as u8
}
struct WavWriter {
file: File,
sample_rate: u32,
channels: u32,
data_bytes: u32,
header: bool,
}
impl WavWriter {
fn new(path: &Path) -> Self {
Self {
file: File::create(path).expect("create wav"),
sample_rate: RATE,
channels: CHANNELS,
data_bytes: 0,
header: false,
}
}
fn write(&mut self, pcm: &[i16], sample_rate: u32, channels: u32) {
if !self.header {
self.sample_rate = sample_rate;
self.channels = channels.max(1);
self.write_header(0);
self.header = true;
}
let mut bytes = Vec::with_capacity(pcm.len() * 2);
for s in pcm {
bytes.extend_from_slice(&s.to_le_bytes());
}
if self.file.write_all(&bytes).is_ok() {
self.data_bytes += bytes.len() as u32;
}
}
fn write_header(&mut self, data_bytes: u32) {
let byte_rate = self.sample_rate * self.channels * 2;
let block_align = (self.channels * 2) as u16;
let _ = self.file.seek(SeekFrom::Start(0));
let _ = self.file.write_all(b"RIFF");
let _ = self.file.write_all(&(36 + data_bytes).to_le_bytes());
let _ = self.file.write_all(b"WAVEfmt ");
let _ = self.file.write_all(&16u32.to_le_bytes());
let _ = self.file.write_all(&1u16.to_le_bytes()); let _ = self.file.write_all(&(self.channels as u16).to_le_bytes());
let _ = self.file.write_all(&self.sample_rate.to_le_bytes());
let _ = self.file.write_all(&byte_rate.to_le_bytes());
let _ = self.file.write_all(&block_align.to_le_bytes());
let _ = self.file.write_all(&16u16.to_le_bytes()); let _ = self.file.write_all(b"data");
let _ = self.file.write_all(&data_bytes.to_le_bytes());
}
fn finalize(&mut self) {
if self.header {
let n = self.data_bytes;
self.write_header(n);
let _ = self.file.seek(SeekFrom::End(0));
}
}
}
}