2026-04-30 15:04:00 -03:00
//! Live media feed controls shared by the launcher and relay child.
use std ::{
fs ,
path ::{ Path , PathBuf } ,
sync ::{ Arc , Mutex } ,
time ::{ SystemTime , UNIX_EPOCH } ,
} ;
2026-05-02 10:31:22 -03:00
use base64 ::{ Engine as _ , engine ::general_purpose ::STANDARD as B64 } ;
2026-05-10 23:14:15 -03:00
use lesavka_common ::audio_transport ::{ UpstreamAudioCodec , parse_upstream_audio_codec } ;
2026-05-02 10:31:22 -03:00
2026-04-30 15:04:00 -03:00
pub const MEDIA_CONTROL_ENV : & str = " LESAVKA_MEDIA_CONTROL " ;
pub const DEFAULT_MEDIA_CONTROL_PATH : & str = " /tmp/lesavka-media.control " ;
2026-05-02 10:31:22 -03:00
#[ derive(Clone, Debug, PartialEq, Eq) ]
pub ( crate ) enum MediaDeviceChoice {
Inherit ,
Auto ,
Selected ( String ) ,
}
2026-05-10 23:14:15 -03:00
#[ derive(Clone, Debug, PartialEq, Eq) ]
pub ( crate ) enum MediaAudioCodecChoice {
Inherit ,
Selected ( UpstreamAudioCodec ) ,
}
impl MediaAudioCodecChoice {
#[ must_use ]
pub fn selected ( codec : UpstreamAudioCodec ) -> Self {
Self ::Selected ( codec )
}
#[ must_use ]
pub fn resolve ( & self , fallback : UpstreamAudioCodec ) -> UpstreamAudioCodec {
match self {
Self ::Inherit = > fallback ,
Self ::Selected ( codec ) = > * codec ,
}
}
}
2026-05-13 11:05:08 -03:00
#[ derive(Clone, Debug, PartialEq, Eq) ]
pub ( crate ) enum MediaCameraCodecChoice {
Inherit ,
Selected ( String ) ,
}
impl MediaCameraCodecChoice {
#[ must_use ]
pub fn selected ( codec : Option < String > ) -> Self {
codec
. filter ( | value | ! value . trim ( ) . is_empty ( ) )
. map ( | value | Self ::Selected ( value . trim ( ) . to_ascii_lowercase ( ) ) )
. unwrap_or ( Self ::Inherit )
}
#[ must_use ]
pub fn resolve ( & self , fallback : Option < & str > ) -> Option < String > {
match self {
Self ::Inherit = > fallback . map ( str ::to_string ) ,
Self ::Selected ( codec ) = > Some ( codec . clone ( ) ) ,
}
}
}
2026-05-10 23:14:15 -03:00
#[ derive(Clone, Debug, PartialEq, Eq) ]
pub ( crate ) enum MediaNoiseSuppressionChoice {
Inherit ,
Enabled ,
Disabled ,
}
impl MediaNoiseSuppressionChoice {
#[ must_use ]
pub fn selected ( enabled : bool ) -> Self {
if enabled {
Self ::Enabled
} else {
Self ::Disabled
}
}
#[ must_use ]
pub fn resolve ( & self , fallback : bool ) -> bool {
match self {
Self ::Inherit = > fallback ,
Self ::Enabled = > true ,
Self ::Disabled = > false ,
}
}
}
2026-05-02 10:31:22 -03:00
impl MediaDeviceChoice {
#[ must_use ]
pub fn from_selection ( selection : Option < String > ) -> Self {
selection
. filter ( | value | ! value . trim ( ) . is_empty ( ) )
. map ( Self ::Selected )
. unwrap_or ( Self ::Auto )
}
#[ must_use ]
2026-05-06 05:50:59 -03:00
/// Keeps `resolve` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
/// Inputs are the typed parameters; output is the return value or side effect.
2026-05-02 10:31:22 -03:00
pub fn resolve ( & self , fallback : Option < & str > ) -> Option < String > {
match self {
Self ::Inherit = > fallback . map ( str ::to_string ) ,
Self ::Auto = > None ,
Self ::Selected ( value ) = > Some ( value . clone ( ) ) ,
}
}
}
#[ derive(Clone, Debug, PartialEq, Eq) ]
2026-04-30 15:04:00 -03:00
pub ( crate ) struct MediaControlState {
pub camera : bool ,
pub microphone : bool ,
pub audio : bool ,
2026-05-02 10:31:22 -03:00
pub camera_source : MediaDeviceChoice ,
pub camera_profile : MediaDeviceChoice ,
pub microphone_source : MediaDeviceChoice ,
pub audio_sink : MediaDeviceChoice ,
2026-05-13 11:05:08 -03:00
pub camera_codec : MediaCameraCodecChoice ,
2026-05-10 23:14:15 -03:00
pub audio_codec : MediaAudioCodecChoice ,
pub noise_suppression : MediaNoiseSuppressionChoice ,
2026-04-30 15:04:00 -03:00
}
impl MediaControlState {
#[ must_use ]
2026-05-02 10:31:22 -03:00
pub fn new ( camera : bool , microphone : bool , audio : bool ) -> Self {
Self {
camera ,
microphone ,
audio ,
camera_source : MediaDeviceChoice ::Inherit ,
camera_profile : MediaDeviceChoice ::Inherit ,
microphone_source : MediaDeviceChoice ::Inherit ,
audio_sink : MediaDeviceChoice ::Inherit ,
2026-05-13 11:05:08 -03:00
camera_codec : MediaCameraCodecChoice ::Inherit ,
2026-05-10 23:14:15 -03:00
audio_codec : MediaAudioCodecChoice ::Inherit ,
noise_suppression : MediaNoiseSuppressionChoice ::Inherit ,
2026-05-02 10:31:22 -03:00
}
}
#[ must_use ]
pub fn with_devices (
camera : bool ,
microphone : bool ,
audio : bool ,
camera_source : Option < String > ,
camera_profile : Option < String > ,
microphone_source : Option < String > ,
audio_sink : Option < String > ,
) -> Self {
2026-04-30 15:04:00 -03:00
Self {
camera ,
microphone ,
audio ,
2026-05-02 10:31:22 -03:00
camera_source : MediaDeviceChoice ::from_selection ( camera_source ) ,
camera_profile : MediaDeviceChoice ::from_selection ( camera_profile ) ,
microphone_source : MediaDeviceChoice ::from_selection ( microphone_source ) ,
audio_sink : MediaDeviceChoice ::from_selection ( audio_sink ) ,
2026-05-13 11:05:08 -03:00
camera_codec : MediaCameraCodecChoice ::Inherit ,
2026-05-10 23:14:15 -03:00
audio_codec : MediaAudioCodecChoice ::Inherit ,
noise_suppression : MediaNoiseSuppressionChoice ::Inherit ,
}
}
#[ must_use ]
#[ allow(clippy::too_many_arguments) ]
pub fn with_devices_and_audio (
camera : bool ,
microphone : bool ,
audio : bool ,
camera_source : Option < String > ,
camera_profile : Option < String > ,
microphone_source : Option < String > ,
audio_sink : Option < String > ,
audio_codec : UpstreamAudioCodec ,
noise_suppression : bool ,
) -> Self {
Self {
audio_codec : MediaAudioCodecChoice ::selected ( audio_codec ) ,
noise_suppression : MediaNoiseSuppressionChoice ::selected ( noise_suppression ) ,
.. Self ::with_devices (
camera ,
microphone ,
audio ,
camera_source ,
camera_profile ,
microphone_source ,
audio_sink ,
)
2026-04-30 15:04:00 -03:00
}
}
2026-05-13 11:05:08 -03:00
#[ must_use ]
#[ allow(clippy::too_many_arguments) ]
pub fn with_devices_and_codecs (
camera : bool ,
microphone : bool ,
audio : bool ,
camera_source : Option < String > ,
camera_profile : Option < String > ,
microphone_source : Option < String > ,
audio_sink : Option < String > ,
camera_codec : Option < String > ,
audio_codec : UpstreamAudioCodec ,
noise_suppression : bool ,
) -> Self {
Self {
camera_codec : MediaCameraCodecChoice ::selected ( camera_codec ) ,
audio_codec : MediaAudioCodecChoice ::selected ( audio_codec ) ,
noise_suppression : MediaNoiseSuppressionChoice ::selected ( noise_suppression ) ,
.. Self ::with_devices (
camera ,
microphone ,
audio ,
camera_source ,
camera_profile ,
microphone_source ,
audio_sink ,
)
}
}
2026-04-30 15:04:00 -03:00
}
#[ derive(Clone, Debug) ]
pub ( crate ) struct LiveMediaControls {
path : PathBuf ,
inner : Arc < Mutex < LiveMediaControlsInner > > ,
}
#[ derive(Debug) ]
struct LiveMediaControlsInner {
state : MediaControlState ,
}
impl LiveMediaControls {
#[ must_use ]
pub fn from_env ( initial : MediaControlState ) -> Self {
let path = std ::env ::var ( MEDIA_CONTROL_ENV )
. map ( PathBuf ::from )
. unwrap_or_else ( | _ | PathBuf ::from ( DEFAULT_MEDIA_CONTROL_PATH ) ) ;
let controls = Self {
path ,
inner : Arc ::new ( Mutex ::new ( LiveMediaControlsInner { state : initial } ) ) ,
} ;
let _ = controls . refresh ( ) ;
controls
}
/// Reloads the launcher-written soft-pause switches, falling back safely on read errors.
pub fn refresh ( & self ) -> MediaControlState {
let Ok ( mut inner ) = self . inner . lock ( ) else {
return MediaControlState ::new ( true , true , true ) ;
} ;
if let Ok ( raw ) = fs ::read_to_string ( & self . path )
& & let Some ( state ) = parse_media_control_state ( & raw )
{
inner . state = state ;
}
2026-05-02 10:31:22 -03:00
inner . state . clone ( )
2026-04-30 15:04:00 -03:00
}
}
2026-05-02 10:31:22 -03:00
/// Writes one atomic-ish soft-pause/device request for the running relay child to poll.
2026-04-30 15:04:00 -03:00
pub ( crate ) fn write_media_control_request (
path : & Path ,
state : MediaControlState ,
) -> std ::io ::Result < ( ) > {
fs ::write (
path ,
format! (
2026-05-13 11:05:08 -03:00
" camera={} microphone={} audio={} camera_source={} camera_profile={} microphone_source={} audio_sink={} camera_codec={} audio_codec={} noise_suppression={} nonce={} \n " ,
2026-04-30 15:04:00 -03:00
bool_flag ( state . camera ) ,
bool_flag ( state . microphone ) ,
bool_flag ( state . audio ) ,
2026-05-02 10:31:22 -03:00
encode_choice ( & state . camera_source ) ,
encode_choice ( & state . camera_profile ) ,
encode_choice ( & state . microphone_source ) ,
encode_choice ( & state . audio_sink ) ,
2026-05-13 11:05:08 -03:00
encode_camera_codec_choice ( & state . camera_codec ) ,
2026-05-10 23:14:15 -03:00
encode_audio_codec_choice ( & state . audio_codec ) ,
encode_noise_suppression_choice ( & state . noise_suppression ) ,
2026-05-02 10:31:22 -03:00
control_request_nonce ( ) ,
2026-04-30 15:04:00 -03:00
) ,
)
}
/// Parses the small launcher control-file grammar used across process boundaries.
fn parse_media_control_state ( raw : & str ) -> Option < MediaControlState > {
let mut camera = None ;
let mut microphone = None ;
let mut audio = None ;
2026-05-02 10:31:22 -03:00
let mut camera_source = MediaDeviceChoice ::Inherit ;
let mut camera_profile = MediaDeviceChoice ::Inherit ;
let mut microphone_source = MediaDeviceChoice ::Inherit ;
let mut audio_sink = MediaDeviceChoice ::Inherit ;
2026-05-13 11:05:08 -03:00
let mut camera_codec = MediaCameraCodecChoice ::Inherit ;
2026-05-10 23:14:15 -03:00
let mut audio_codec = MediaAudioCodecChoice ::Inherit ;
let mut noise_suppression = MediaNoiseSuppressionChoice ::Inherit ;
2026-04-30 15:04:00 -03:00
for token in raw . split_ascii_whitespace ( ) {
let Some ( ( key , value ) ) = token . split_once ( '=' ) else {
continue ;
} ;
match key {
" camera " = > camera = Some ( parse_bool_flag ( value ) ? ) ,
" microphone " | " mic " = > microphone = Some ( parse_bool_flag ( value ) ? ) ,
" audio " | " speaker " = > audio = Some ( parse_bool_flag ( value ) ? ) ,
2026-05-02 10:31:22 -03:00
" camera_source " | " camera_source_b64 " = > camera_source = parse_choice ( value ) ? ,
" camera_profile " | " camera_quality " = > camera_profile = parse_choice ( value ) ? ,
" microphone_source " | " mic_source " | " microphone_source_b64 " = > {
microphone_source = parse_choice ( value ) ? ;
}
" audio_sink " | " speaker_sink " | " audio_sink_b64 " = > audio_sink = parse_choice ( value ) ? ,
2026-05-13 11:05:08 -03:00
" camera_codec " | " uplink_camera_codec " | " webcam_transport " = > {
camera_codec = parse_camera_codec_choice ( value ) ? ;
}
2026-05-10 23:14:15 -03:00
" audio_codec " | " uplink_audio_codec " = > {
audio_codec = parse_audio_codec_choice ( value ) ? ;
}
" noise_suppression " | " mic_noise_suppression " = > {
noise_suppression = parse_noise_suppression_choice ( value ) ? ;
}
2026-04-30 15:04:00 -03:00
_ = > { }
}
}
Some ( MediaControlState {
camera : camera ? ,
microphone : microphone ? ,
audio : audio ? ,
2026-05-02 10:31:22 -03:00
camera_source ,
camera_profile ,
microphone_source ,
audio_sink ,
2026-05-13 11:05:08 -03:00
camera_codec ,
2026-05-10 23:14:15 -03:00
audio_codec ,
noise_suppression ,
2026-04-30 15:04:00 -03:00
} )
}
2026-05-06 05:50:59 -03:00
/// Keeps `encode_choice` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
/// Inputs are the typed parameters; output is the return value or side effect.
2026-05-02 10:31:22 -03:00
fn encode_choice ( choice : & MediaDeviceChoice ) -> String {
match choice {
MediaDeviceChoice ::Inherit = > " inherit " . to_string ( ) ,
MediaDeviceChoice ::Auto = > " auto " . to_string ( ) ,
MediaDeviceChoice ::Selected ( value ) = > format! ( " b64: {} " , B64 . encode ( value . as_bytes ( ) ) ) ,
}
}
2026-05-06 05:50:59 -03:00
/// Keeps `parse_choice` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
/// Inputs are the typed parameters; output is the return value or side effect.
2026-05-02 10:31:22 -03:00
fn parse_choice ( value : & str ) -> Option < MediaDeviceChoice > {
let value = value . trim ( ) ;
if value . is_empty ( ) | | value . eq_ignore_ascii_case ( " auto " ) {
return Some ( MediaDeviceChoice ::Auto ) ;
}
if value . eq_ignore_ascii_case ( " inherit " ) {
return Some ( MediaDeviceChoice ::Inherit ) ;
}
if let Some ( encoded ) = value . strip_prefix ( " b64: " ) {
let decoded = B64 . decode ( encoded ) . ok ( ) ? ;
let decoded = String ::from_utf8 ( decoded ) . ok ( ) ? ;
return Some ( MediaDeviceChoice ::from_selection ( Some ( decoded ) ) ) ;
}
Some ( MediaDeviceChoice ::from_selection ( Some ( value . to_string ( ) ) ) )
}
2026-05-13 11:05:08 -03:00
fn encode_camera_codec_choice ( choice : & MediaCameraCodecChoice ) -> & str {
match choice {
MediaCameraCodecChoice ::Inherit = > " inherit " ,
MediaCameraCodecChoice ::Selected ( codec ) = > codec . as_str ( ) ,
}
}
fn parse_camera_codec_choice ( value : & str ) -> Option < MediaCameraCodecChoice > {
let value = value . trim ( ) ;
if value . eq_ignore_ascii_case ( " inherit " ) | | value . is_empty ( ) {
return Some ( MediaCameraCodecChoice ::Inherit ) ;
}
match value . to_ascii_lowercase ( ) . as_str ( ) {
" mjpeg " | " mjpg " | " jpeg " = > Some ( MediaCameraCodecChoice ::Selected ( " mjpeg " . to_string ( ) ) ) ,
" hevc " | " h265 " | " h.265 " = > Some ( MediaCameraCodecChoice ::Selected ( " hevc " . to_string ( ) ) ) ,
" h264 " = > Some ( MediaCameraCodecChoice ::Selected ( " h264 " . to_string ( ) ) ) ,
_ = > None ,
}
}
2026-05-10 23:14:15 -03:00
fn encode_audio_codec_choice ( choice : & MediaAudioCodecChoice ) -> & 'static str {
match choice {
MediaAudioCodecChoice ::Inherit = > " inherit " ,
MediaAudioCodecChoice ::Selected ( codec ) = > codec . as_id ( ) ,
}
}
fn parse_audio_codec_choice ( value : & str ) -> Option < MediaAudioCodecChoice > {
let value = value . trim ( ) ;
if value . eq_ignore_ascii_case ( " inherit " ) | | value . is_empty ( ) {
return Some ( MediaAudioCodecChoice ::Inherit ) ;
}
parse_upstream_audio_codec ( value ) . map ( MediaAudioCodecChoice ::Selected )
}
fn encode_noise_suppression_choice ( choice : & MediaNoiseSuppressionChoice ) -> & 'static str {
match choice {
MediaNoiseSuppressionChoice ::Inherit = > " inherit " ,
MediaNoiseSuppressionChoice ::Enabled = > " 1 " ,
MediaNoiseSuppressionChoice ::Disabled = > " 0 " ,
}
}
fn parse_noise_suppression_choice ( value : & str ) -> Option < MediaNoiseSuppressionChoice > {
if value . trim ( ) . eq_ignore_ascii_case ( " inherit " ) | | value . trim ( ) . is_empty ( ) {
return Some ( MediaNoiseSuppressionChoice ::Inherit ) ;
}
parse_bool_flag ( value ) . map ( MediaNoiseSuppressionChoice ::selected )
}
2026-05-06 05:50:59 -03:00
/// Keeps `parse_bool_flag` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
/// Inputs are the typed parameters; output is the return value or side effect.
2026-04-30 15:04:00 -03:00
fn parse_bool_flag ( value : & str ) -> Option < bool > {
match value . trim ( ) . to_ascii_lowercase ( ) . as_str ( ) {
" 1 " | " true " | " on " | " yes " = > Some ( true ) ,
" 0 " | " false " | " off " | " no " = > Some ( false ) ,
_ = > None ,
}
}
const fn bool_flag ( enabled : bool ) -> & 'static str {
if enabled { " 1 " } else { " 0 " }
}
fn control_request_nonce ( ) -> u128 {
SystemTime ::now ( )
. duration_since ( UNIX_EPOCH )
. map ( | duration | duration . as_nanos ( ) )
. unwrap_or_default ( )
}
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ test ]
fn parses_media_control_state_from_launcher_file ( ) {
assert_eq! (
parse_media_control_state ( " camera=1 microphone=0 audio=true 123 " ) ,
Some ( MediaControlState ::new ( true , false , true ) )
) ;
}
2026-05-02 10:31:22 -03:00
#[ test ]
2026-05-06 05:50:59 -03:00
/// Keeps `parses_media_control_state_with_live_device_choices` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
/// Inputs are the typed parameters; output is the return value or side effect.
2026-05-02 10:31:22 -03:00
fn parses_media_control_state_with_live_device_choices ( ) {
let state = MediaControlState ::with_devices (
true ,
true ,
true ,
Some ( " Logitech BRIO " . to_string ( ) ) ,
Some ( " 1280x720@30 " . to_string ( ) ) ,
Some ( " alsa_input.usb-Neat Microphones " . to_string ( ) ) ,
None ,
) ;
let dir = tempfile ::tempdir ( ) . expect ( " tempdir " ) ;
let path = dir . path ( ) . join ( " media.control " ) ;
write_media_control_request ( & path , state . clone ( ) ) . expect ( " write controls " ) ;
let raw = fs ::read_to_string ( path ) . expect ( " read controls " ) ;
assert_eq! ( parse_media_control_state ( & raw ) , Some ( state ) ) ;
}
#[ test ]
fn device_choices_resolve_inherit_auto_and_selected ( ) {
assert_eq! (
MediaDeviceChoice ::Inherit . resolve ( Some ( " env-device " ) ) ,
Some ( " env-device " . to_string ( ) )
) ;
assert_eq! ( MediaDeviceChoice ::Auto . resolve ( Some ( " env-device " ) ) , None ) ;
assert_eq! (
MediaDeviceChoice ::Selected ( " chosen " . to_string ( ) ) . resolve ( Some ( " env-device " ) ) ,
Some ( " chosen " . to_string ( ) )
) ;
}
2026-04-30 15:04:00 -03:00
#[ test ]
2026-05-06 05:50:59 -03:00
/// Keeps `live_media_controls_refresh_after_file_changes` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
/// Inputs are the typed parameters; output is the return value or side effect.
2026-04-30 15:04:00 -03:00
fn live_media_controls_refresh_after_file_changes ( ) {
let dir = tempfile ::tempdir ( ) . expect ( " tempdir " ) ;
let path = dir . path ( ) . join ( " media.control " ) ;
write_media_control_request ( & path , MediaControlState ::new ( true , true , false ) )
. expect ( " write initial controls " ) ;
let controls = LiveMediaControls {
path : path . clone ( ) ,
inner : Arc ::new ( Mutex ::new ( LiveMediaControlsInner {
state : MediaControlState ::new ( false , false , false ) ,
} ) ) ,
} ;
assert_eq! (
controls . refresh ( ) ,
MediaControlState ::new ( true , true , false )
) ;
std ::thread ::sleep ( std ::time ::Duration ::from_millis ( 5 ) ) ;
write_media_control_request ( & path , MediaControlState ::new ( false , true , true ) )
. expect ( " write updated controls " ) ;
assert_eq! (
controls . refresh ( ) ,
MediaControlState ::new ( false , true , true )
) ;
}
#[ test ]
fn from_env_default_path_fallback_is_safe ( ) {
let controls = LiveMediaControls ::from_env ( MediaControlState ::new ( true , false , true ) ) ;
let _ = controls . refresh ( ) ;
}
#[ test ]
fn parser_tolerates_unknown_tokens_and_rejects_invalid_flags ( ) {
assert_eq! (
parse_media_control_state ( " camera=on extra=ignored microphone=no audio=off " ) ,
Some ( MediaControlState ::new ( true , false , false ) )
) ;
assert_eq! (
parse_media_control_state ( " camera=maybe microphone=1 audio=1 " ) ,
None
) ;
}
#[ test ]
2026-05-06 05:50:59 -03:00
/// Keeps `refresh_falls_back_to_all_enabled_if_lock_is_poisoned` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
/// Inputs are the typed parameters; output is the return value or side effect.
2026-04-30 15:04:00 -03:00
fn refresh_falls_back_to_all_enabled_if_lock_is_poisoned ( ) {
let controls = LiveMediaControls {
path : PathBuf ::from ( " /definitely/not/a/real/lesavka-media.control " ) ,
inner : Arc ::new ( Mutex ::new ( LiveMediaControlsInner {
state : MediaControlState ::new ( false , false , false ) ,
} ) ) ,
} ;
let inner = Arc ::clone ( & controls . inner ) ;
let _ = std ::panic ::catch_unwind ( move | | {
let _guard = inner . lock ( ) . expect ( " lock " ) ;
panic! ( " poison media controls lock " ) ;
} ) ;
assert_eq! ( controls . refresh ( ) , MediaControlState ::new ( true , true , true ) ) ;
}
}