use std::
{
env,
thread,
time::{ Duration, Instant },
sync::
{
Arc,
RwLock,
atomic::{ AtomicBool, Ordering },
mpsc::Receiver,
},
};
#[cfg(not(target_os = "macos"))]
use std::sync::mpsc;
use std::sync::mpsc::RecvTimeoutError;
use tokio::sync::mpsc::Sender;
use xcap::{ Frame, Monitor, VideoRecorder };
use openh264::
{
OpenH264API,
formats::{ RgbaSliceU8, YUVBuffer },
encoder::
{
Encoder,
EncoderConfig,
BitRate,
FrameRate,
IntraFramePeriod,
Complexity,
UsageType,
RateControlMode,
},
};
use crate::network::screen::
{
consts,
client::{ gpu::GpuConverter, options },
};
fn monitor_name(monitor: &Monitor) -> String
{
monitor.name().unwrap_or_else(|_| "unknown".to_owned())
}
fn monitor_list(monitors: &[Monitor]) -> String {
monitors.iter().enumerate()
.map(|(index, monitor)| format!("{} ({})", index + 1, monitor_name(monitor)))
.collect::<Vec<String>>()
.join(", ")
}
fn select_monitor(monitors: Vec<Monitor>, selection: &str) -> Result<Monitor, String>
{
if let Ok(index) = selection.parse::<usize>()
&& let Some(monitor) = index.checked_sub(1).and_then(|index| monitors.get(index))
{
return Ok(monitor.clone());
}
monitors.iter()
.find(|monitor| monitor_name(monitor).eq_ignore_ascii_case(selection))
.cloned()
.ok_or_else(|| format!("no monitor called '{selection}' - available: {}", monitor_list(&monitors)))
}
fn get_target_monitor() -> Result<Monitor, String> {
let monitors = Monitor::all().map_err(|e| format!("failed to enumerate monitors ({e})"))?;
if monitors.is_empty() { return Err("no monitors found".to_owned()); }
match options::get_monitor()
{
Some(selection) => select_monitor(monitors, &selection),
None => Ok(monitors.iter()
.find(|m| m.is_primary().unwrap_or(false))
.cloned()
.unwrap_or_else(|| monitors.into_iter().next().unwrap())),
}
}
pub fn resolve_monitor(selection: &str) -> Result<String, String>
{
let monitors = Monitor::all().map_err(|e| format!("failed to enumerate monitors ({e})"))?;
select_monitor(monitors, selection).map(|monitor| monitor_name(&monitor))
}
pub fn current_monitor() -> Option<String> {
get_target_monitor().ok().map(|monitor| monitor_name(&monitor))
}
pub fn monitor_names() -> Vec<String>
{
static CACHE: RwLock<Option<(Instant, Vec<String>)>> = RwLock::new(None);
if let Some((taken, names)) = CACHE.read().unwrap().as_ref()
&& taken.elapsed() < consts::MONITOR_LIST_TTL
{
return names.clone();
}
let names = Monitor::all().map(|monitors| monitors.iter().map(monitor_name).collect::<Vec<String>>())
.unwrap_or_default();
*CACHE.write().unwrap() = Some((Instant::now(), names.clone()));
names
}
static UPGRADING: AtomicBool = AtomicBool::new(false);
fn upgrading() -> bool
{
UPGRADING.load(Ordering::Relaxed)
}
#[cfg(target_os = "linux")]
fn wayland() -> bool
{
env::var("WAYLAND_DISPLAY").is_ok() || env::var("XDG_SESSION_TYPE").unwrap_or_default() == "wayland"
}
fn legacy_capture_loop (
frame_tx: Sender<Vec<u8>>,
running: Arc<AtomicBool>,
fps: u32,
) -> Result<(), String>
{
#[cfg(target_os = "linux")]
if wayland()
{
return capture_loop_wayshot(frame_tx, running, fps);
}
capture_loop_xcap(get_target_monitor()?, frame_tx, running, fps)
}
pub fn capture_loop (
frame_tx: Sender<Vec<u8>>,
running: Arc<AtomicBool>,
fps: u32,
) -> Result<(), String>
{
loop
{
let generation = options::monitor_generation();
let outcome = capture_backend(frame_tx.clone(), running.clone(), fps);
if !switched(generation) || !running.load(Ordering::Relaxed) || !options::get_use_screen()
{
return outcome;
}
}
}
fn switched(generation: usize) -> bool {
options::monitor_generation() != generation
}
fn capture_backend (
frame_tx: Sender<Vec<u8>>,
running: Arc<AtomicBool>,
fps: u32,
) -> Result<(), String>
{
match env::var(consts::BACKEND_OVERRIDE_VAR).unwrap_or_default().to_lowercase().as_str()
{
"recorder" => return capture_loop_recorder(frame_tx, running, fps),
"legacy" | "xcap" | "wayshot" => return legacy_capture_loop(frame_tx, running, fps),
_ => {},
}
#[cfg(target_os = "linux")]
if wayland() && options::get_monitor().is_some()
{
return legacy_capture_loop(frame_tx, running, fps);
}
#[cfg(target_os = "macos")]
return match open_recorder()
{
Ok(session) => run_recorder(session, frame_tx, running, fps),
Err(_) => legacy_capture_loop(frame_tx, running, fps),
};
#[cfg(not(target_os = "macos"))]
{
UPGRADING.store(false, Ordering::Relaxed);
let (probe_tx, probe_rx) = mpsc::channel();
thread::spawn(move ||
{
let session = open_recorder();
if session.is_ok() { UPGRADING.store(true, Ordering::Relaxed); }
probe_tx.send(session).ok();
});
let outcome = legacy_capture_loop(frame_tx.clone(), running.clone(), fps);
if !upgrading() && (outcome.is_ok() || !running.load(Ordering::Relaxed)) { return outcome; }
let probed = if upgrading()
{
probe_rx.recv().ok()
} else
{
probe_rx.recv_timeout(probe_timeout()).ok()
};
UPGRADING.store(false, Ordering::Relaxed);
match probed
{
Some(Ok(session)) if running.load(Ordering::Relaxed) => run_recorder(session, frame_tx, running, fps),
_ => outcome,
}
}
}
fn create_encoder(fps: f32) -> Result<Encoder, String>
{
let config = EncoderConfig::new()
.max_frame_rate(FrameRate::from_hz(fps))
.rate_control_mode(RateControlMode::Bitrate)
.bitrate(BitRate::from_bps(consts::H264_BITRATE))
.intra_frame_period(IntraFramePeriod::from_num_frames((fps * 2.0) as u32))
.complexity(Complexity::Low)
.usage_type(UsageType::ScreenContentRealTime)
.skip_frames(true)
.adaptive_quantization(false)
.background_detection(false);
Encoder::with_api_config(OpenH264API::from_source(), config)
.map_err(|e| format!("failed to create H.264 encoder ({e})"))
}
struct YuvScratch {
buffer: YUVBuffer,
width: u32,
height: u32,
}
impl YuvScratch
{
fn new() -> Self
{
Self { buffer: YUVBuffer::new(0, 0), width: 0, height: 0 }
}
fn fill(&mut self, width: u32, height: u32, rgba: &[u8]) -> &YUVBuffer
{
if self.width != width || self.height != height
{
self.buffer = YUVBuffer::new(width as usize, height as usize);
self.width = width;
self.height = height;
}
self.buffer.read_rgb(RgbaSliceU8::new(rgba, (width as usize, height as usize)));
&self.buffer
}
}
enum Converter {
Gpu(Box<GpuConverter>),
Cpu(YuvScratch),
}
impl Converter
{
fn select() -> Self
{
if env::var(consts::CONVERTER_OVERRIDE_VAR).unwrap_or_default().eq_ignore_ascii_case("cpu")
{
return Converter::Cpu(YuvScratch::new());
}
match GpuConverter::new()
{
Ok(converter) => Converter::Gpu(Box::new(converter)),
Err(_) => Converter::Cpu(YuvScratch::new()),
}
}
}
struct FrameEncoder
{
encoder: Encoder,
converter: Converter,
fps: f32,
dimensions: Option<(u32, u32)>,
}
impl FrameEncoder
{
fn new(fps: f32) -> Result<Self, String>
{
Ok(Self { encoder: create_encoder(fps)?, converter: Converter::select(), fps, dimensions: None })
}
fn force_intra_frame(&mut self)
{
self.encoder.force_intra_frame();
}
fn encode(&mut self, width: u32, height: u32, rgba: &[u8]) -> Result<Option<Vec<u8>>, String>
{
if width % 2 != 0 || height % 2 != 0
{
return Err(format!("unsupported capture resolution {width}x{height} (must be even)"));
}
if self.dimensions.is_some_and(|previous| previous != (width, height))
{
self.encoder = create_encoder(self.fps)?;
}
self.dimensions = Some((width, height));
if let Converter::Gpu(_) = &self.converter
&& !GpuConverter::supports(width, height)
{
self.converter = Converter::Cpu(YuvScratch::new());
}
let mut fallback = None;
let bitstream = match &mut self.converter
{
Converter::Gpu(converter) => match converter.convert(width, height, rgba)
{
Ok(frame) =>
{
let bitstream = self.encoder.encode(frame)
.map_err(|e| format!("H.264 encode failed ({e})"))?;
Some(bitstream.to_vec())
},
Err(reason) =>
{
fallback = Some(reason);
None
},
},
Converter::Cpu(scratch) =>
{
let yuv = scratch.fill(width, height, rgba);
let bitstream = self.encoder.encode(yuv)
.map_err(|e| format!("H.264 encode failed ({e})"))?;
Some(bitstream.to_vec())
},
};
let data = match bitstream
{
Some(result) => result,
None =>
{
debug_assert!(fallback.is_some(), "the GPU path only yields None after refusing a frame");
self.converter = Converter::Cpu(YuvScratch::new());
let Converter::Cpu(scratch) = &mut self.converter else { unreachable!() };
let yuv = scratch.fill(width, height, rgba);
let bitstream = self.encoder.encode(yuv)
.map_err(|e| format!("H.264 encode failed ({e})"))?;
bitstream.to_vec()
},
};
if data.is_empty()
{
return Ok(None);
}
Ok(Some(data))
}
fn dispatch(&mut self, frame_tx: &Sender<Vec<u8>>, frame: Vec<u8>) {
if frame_tx.try_send(frame).is_err()
{
self.force_intra_frame();
}
}
}
fn sleep_until_next_tick(next_tick: &mut Instant, target_interval: Duration)
{
let now = Instant::now();
if *next_tick > now
{
thread::sleep(*next_tick - now);
} else
{
*next_tick = now;
}
*next_tick += target_interval;
}
fn capture_loop_xcap
(
monitor: Monitor,
frame_tx: Sender<Vec<u8>>,
running: Arc<AtomicBool>,
fps: u32,
) -> Result<(), String>
{
let target_interval = Duration::from_secs_f64(1.0 / fps as f64);
let mut next_tick = Instant::now() + target_interval;
let generation = options::monitor_generation();
let mut encoder = FrameEncoder::new(fps as f32)?;
let mut last_image: Option<xcap::image::RgbaImage> = None;
let mut last_encode_time = Instant::now();
while running.load(Ordering::Relaxed) && !upgrading() && !switched(generation)
{
if !options::get_use_screen()
{
running.store(false, Ordering::Relaxed);
return Ok(());
}
if let Ok(image) = monitor.capture_image()
{
let force_encode = last_encode_time.elapsed() >= consts::FORCED_INTRA_INTERVAL;
let changed = last_image.as_ref().is_none_or(|previous| previous.as_raw() != image.as_raw());
if force_encode || changed
{
if let Some(compressed) = encoder.encode(image.width(), image.height(), image.as_raw())?
{
encoder.dispatch(&frame_tx, compressed);
}
last_image = Some(image);
last_encode_time = Instant::now();
}
}
sleep_until_next_tick(&mut next_tick, target_interval);
}
Ok(())
}
#[cfg(target_os = "linux")]
fn select_output(wayshot: &libwayshot::WayshotConnection) -> Result<libwayshot::output::OutputInfo, String> {
let outputs = wayshot.get_all_outputs();
if outputs.is_empty() { return Err("compositor reported no outputs".to_owned()); }
let picked = options::get_monitor().is_some();
match get_target_monitor().and_then(|m| m.name().map_err(|e| e.to_string()))
{
Ok(name) => match outputs.iter().find(|o| o.name == name)
{
Some(output) => return Ok(output.clone()),
None if picked => return Err(format!("the compositor knows no output called '{name}'")),
None => {},
},
Err(reason) if picked => return Err(reason),
Err(_) => {},
}
Ok(outputs.iter()
.find(|o| o.logical_region.inner.position.x == 0 && o.logical_region.inner.position.y == 0)
.unwrap_or(&outputs[0])
.clone())
}
#[cfg(target_os = "linux")]
fn reconnect_wayshot(name: &str) -> Option<(libwayshot::WayshotConnection, libwayshot::output::OutputInfo)>
{
let connection = libwayshot::WayshotConnection::new().ok()?;
let output = connection.get_all_outputs().iter().find(|output| output.name == name).cloned()?;
Some((connection, output))
}
#[cfg(target_os = "linux")]
fn capture_loop_wayshot
(
frame_tx: Sender<Vec<u8>>,
running: Arc<AtomicBool>,
fps: u32,
) -> Result<(), String>
{
let target_interval = Duration::from_secs_f64(1.0 / fps as f64);
let generation = options::monitor_generation();
let mut wayshot = libwayshot::WayshotConnection::new()
.map_err(|e| format!("wayland screen capture is unavailable ({e})"))?;
let mut target_output = select_output(&wayshot)?;
let mut encoder = FrameEncoder::new(fps as f32)?;
let first_image = wayshot.screenshot_single_output(&target_output, true)
.map_err(|e| format!("capturing {} failed ({e}) - your compositor must support \
ext-image-copy-capture-v1 or wlr-screencopy-v1", target_output.name))?
.into_rgba8();
if let Some(compressed) = encoder.encode(first_image.width(), first_image.height(), first_image.as_raw())?
{
encoder.dispatch(&frame_tx, compressed);
}
let mut last_image = Some(first_image);
let mut last_encode_time = Instant::now();
let mut failures = 0u32;
let mut next_tick = Instant::now() + target_interval;
let mut stranded = 0u64;
while running.load(Ordering::Relaxed) && !upgrading() && !switched(generation)
{
if !options::get_use_screen()
{
running.store(false, Ordering::Relaxed);
return Ok(());
}
match wayshot.screenshot_single_output(&target_output, true)
{
Ok(image) =>
{
failures = 0;
let image = image.into_rgba8();
stranded += image.as_raw().len() as u64;
let force_encode = last_encode_time.elapsed() >= consts::FORCED_INTRA_INTERVAL;
let changed = last_image.as_ref().is_none_or(|previous| previous.as_raw() != image.as_raw());
if force_encode || changed
{
if let Some(compressed) = encoder.encode(image.width(), image.height(), image.as_raw())?
{
encoder.dispatch(&frame_tx, compressed);
}
last_image = Some(image);
last_encode_time = Instant::now();
}
if stranded >= consts::WAYLAND_LEAK_BUDGET
{
if let Some((connection, output)) = reconnect_wayshot(&target_output.name)
{
wayshot = connection;
target_output = output;
}
stranded = 0;
}
},
Err(_) =>
{
failures += 1;
if failures >= consts::WAYLAND_RECONNECT_FAILURES
{
if let Some((connection, output)) = reconnect_wayshot(&target_output.name)
{
wayshot = connection;
target_output = output;
encoder.force_intra_frame();
last_image = None;
}
stranded = 0;
failures = 0;
}
},
}
sleep_until_next_tick(&mut next_tick, target_interval);
}
Ok(())
}
struct RecorderSession {
recorder: VideoRecorder,
frames: Receiver<Frame>,
first: Frame,
}
fn open_recorder() -> Result<RecorderSession, String> {
let monitor = get_target_monitor()?;
let (recorder, frames) = monitor.video_recorder()
.map_err(|e| format!("the OS screen recorder is unavailable ({e})"))?;
recorder.start()
.map_err(|e| format!("starting the OS screen recorder failed ({e})"))?;
let first = frames.recv_timeout(consts::RECORDER_FIRST_FRAME)
.map_err(|_| "the OS screen recorder started but delivered no frames".to_owned())?;
Ok(RecorderSession { recorder, frames, first })
}
#[cfg(not(target_os = "macos"))]
fn probe_timeout() -> Duration
{
env::var(consts::PROBE_TIMEOUT_VAR).ok()
.and_then(|value| value.parse().ok())
.map(Duration::from_secs)
.unwrap_or(consts::RECORDER_PROBE_TIMEOUT)
}
#[cfg(target_os = "macos")]
fn start_recorder() -> Result<RecorderSession, String>
{
open_recorder()
}
#[cfg(not(target_os = "macos"))]
fn start_recorder() -> Result<RecorderSession, String> {
let (probe_tx, probe_rx) = mpsc::channel();
thread::spawn(move ||
{
probe_tx.send(open_recorder()).ok();
});
match probe_rx.recv_timeout(probe_timeout())
{
Ok(result) => result,
Err(RecvTimeoutError::Timeout) => Err("the OS screen recorder did not answer in time".to_owned()),
Err(RecvTimeoutError::Disconnected) => Err("the OS screen recorder probe died".to_owned()),
}
}
fn run_recorder (
session: RecorderSession,
frame_tx: Sender<Vec<u8>>,
running: Arc<AtomicBool>,
fps: u32,
) -> Result<(), String>
{
let RecorderSession { recorder, frames, first } = session;
let mut encoder = FrameEncoder::new(fps as f32)?;
let min_interval = Duration::from_secs_f64(1.0 / fps as f64);
let mut last_encode_time = Instant::now();
let mut last_dispatch = Instant::now() - min_interval;
let mut last_raw: Option<Vec<u8>> = None;
let mut pending = Some(first);
let generation = options::monitor_generation();
let outcome = loop
{
if !running.load(Ordering::Relaxed) { break Ok(()); }
if switched(generation) { break Ok(()); }
if !options::get_use_screen()
{
running.store(false, Ordering::Relaxed);
break Ok(());
}
let mut frame = match pending.take()
{
Some(frame) => frame,
None => match frames.recv_timeout(consts::RECORDER_POLL_INTERVAL)
{
Ok(frame) => frame,
Err(RecvTimeoutError::Timeout) => continue,
Err(RecvTimeoutError::Disconnected) => break Err("the OS screen recorder stopped delivering frames".to_owned()),
},
};
while let Ok(newer) = frames.try_recv()
{
frame = newer;
}
let force_encode = last_encode_time.elapsed() >= consts::FORCED_INTRA_INTERVAL;
if !force_encode && last_dispatch.elapsed() < min_interval
{
continue;
}
let changed = last_raw.as_ref().is_none_or(|previous| previous != &frame.raw);
if !(force_encode || changed)
{
continue;
}
if let Some(compressed) = encoder.encode(frame.width, frame.height, &frame.raw)?
{
encoder.dispatch(&frame_tx, compressed);
}
last_dispatch = Instant::now();
last_encode_time = last_dispatch;
last_raw = Some(frame.raw);
};
recorder.stop().ok();
outcome
}
fn capture_loop_recorder (
frame_tx: Sender<Vec<u8>>,
running: Arc<AtomicBool>,
fps: u32,
) -> Result<(), String>
{
run_recorder(start_recorder()?, frame_tx, running, fps)
}