2026-05-16 06:48:01 -03:00
#![ forbid(unsafe_code) ]
use std ::{ path ::PathBuf , time ::Duration } ;
use anyhow ::{ Context , Result , bail } ;
use gstreamer as gst ;
use gstreamer ::prelude ::* ;
use gstreamer_app as gst_app ;
use lesavka_common ::lesavka ::{
AudioEncoding , AudioPacket , UpstreamMediaBundle , VideoPacket , relay_client ::RelayClient ,
} ;
use tokio ::sync ::mpsc ;
use tokio_stream ::wrappers ::ReceiverStream ;
use tonic ::Request ;
2026-05-16 22:03:12 -03:00
use tonic ::transport ::{ Certificate , Channel , ClientTlsConfig , Identity } ;
2026-05-16 06:48:01 -03:00
const DEFAULT_SERVER : & str = " http://127.0.0.1:50051 " ;
const DEFAULT_SAMPLE_RATE : u32 = 48_000 ;
const DEFAULT_CHANNELS : u32 = 2 ;
2026-05-17 00:42:08 -03:00
const DEFAULT_JPEG_QUALITY : i32 = 82 ;
2026-05-16 06:48:01 -03:00
const MARKER_BITS : usize = 32 ;
const MARKER_COLUMNS : usize = 16 ;
#[ derive(Clone, Debug) ]
struct Args {
server : String ,
width : usize ,
height : usize ,
fps : u32 ,
duration : Duration ,
jpeg_quality : i32 ,
session_id : u64 ,
artifact_dir : Option < PathBuf > ,
print_every : u64 ,
2026-05-17 00:42:08 -03:00
max_frame_bytes : usize ,
2026-05-16 22:03:12 -03:00
tls_ca : Option < PathBuf > ,
tls_client_cert : Option < PathBuf > ,
tls_client_key : Option < PathBuf > ,
tls_domain : Option < String > ,
2026-05-16 06:48:01 -03:00
}
impl Args {
fn parse ( ) -> Result < Self > {
let mut args = Self {
server : DEFAULT_SERVER . to_string ( ) ,
width : 1280 ,
height : 720 ,
fps : 30 ,
duration : Duration ::from_secs ( 300 ) ,
jpeg_quality : DEFAULT_JPEG_QUALITY ,
session_id : unix_millis ( ) ,
artifact_dir : None ,
print_every : 150 ,
2026-05-17 00:42:08 -03:00
max_frame_bytes : env_usize ( " LESAVKA_SYNTHETIC_MAX_FRAME_BYTES " ) . unwrap_or ( 0 ) ,
2026-05-16 22:03:12 -03:00
tls_ca : env_path ( " LESAVKA_TLS_CA " ) . or_else ( | | default_pki_path ( " ca.crt " ) ) ,
tls_client_cert : env_path ( " LESAVKA_TLS_CLIENT_CERT " )
. or_else ( | | default_pki_path ( " client.crt " ) ) ,
tls_client_key : env_path ( " LESAVKA_TLS_CLIENT_KEY " )
. or_else ( | | default_pki_path ( " client.key " ) ) ,
tls_domain : std ::env ::var ( " LESAVKA_TLS_DOMAIN " )
. ok ( )
. filter ( | value | ! value . trim ( ) . is_empty ( ) ) ,
2026-05-16 06:48:01 -03:00
} ;
let mut it = std ::env ::args ( ) . skip ( 1 ) ;
while let Some ( flag ) = it . next ( ) {
match flag . as_str ( ) {
" --server " = > args . server = next_value ( & mut it , & flag ) ? ,
" --width " = > args . width = parse_next ( & mut it , & flag ) ? ,
" --height " = > args . height = parse_next ( & mut it , & flag ) ? ,
" --fps " = > args . fps = parse_next ( & mut it , & flag ) ? ,
" --duration " = > {
let seconds : f64 = parse_next ( & mut it , & flag ) ? ;
args . duration = Duration ::from_secs_f64 ( seconds . max ( 0.0 ) ) ;
}
" --jpeg-quality " = > args . jpeg_quality = parse_next ( & mut it , & flag ) ? ,
" --session-id " = > args . session_id = parse_next ( & mut it , & flag ) ? ,
" --artifact-dir " = > {
args . artifact_dir = Some ( PathBuf ::from ( next_value ( & mut it , & flag ) ? ) )
}
" --print-every " = > args . print_every = parse_next ( & mut it , & flag ) ? ,
2026-05-17 00:42:08 -03:00
" --max-frame-bytes " = > args . max_frame_bytes = parse_next ( & mut it , & flag ) ? ,
2026-05-16 22:03:12 -03:00
" --tls-ca " = > args . tls_ca = Some ( PathBuf ::from ( next_value ( & mut it , & flag ) ? ) ) ,
" --tls-client-cert " = > {
args . tls_client_cert = Some ( PathBuf ::from ( next_value ( & mut it , & flag ) ? ) )
}
" --tls-client-key " = > {
args . tls_client_key = Some ( PathBuf ::from ( next_value ( & mut it , & flag ) ? ) )
}
" --tls-domain " = > args . tls_domain = Some ( next_value ( & mut it , & flag ) ? ) ,
2026-05-16 06:48:01 -03:00
" --mode " = > {
let value = next_value ( & mut it , & flag ) ? ;
let ( width , height , fps ) = parse_mode ( & value ) ? ;
args . width = width ;
args . height = height ;
args . fps = fps ;
}
" --help " | " -h " = > {
print_help ( ) ;
std ::process ::exit ( 0 ) ;
}
other = > bail! ( " unknown argument {other:?}; pass --help for usage " ) ,
}
}
if args . width = = 0 | | args . height = = 0 | | args . fps = = 0 {
bail! ( " width, height, and fps must be positive " ) ;
}
args . jpeg_quality = args . jpeg_quality . clamp ( 1 , 100 ) ;
Ok ( args )
}
fn frame_step_us ( & self ) -> u64 {
( 1_000_000_ u64 / u64 ::from ( self . fps ) ) . max ( 1 )
}
fn total_frames ( & self ) -> u64 {
let frames = self . duration . as_secs_f64 ( ) * f64 ::from ( self . fps ) ;
frames . ceil ( ) . max ( 1.0 ) as u64
}
}
struct MjpegEncoder {
src : gst_app ::AppSrc ,
sink : gst_app ::AppSink ,
pipeline : gst ::Pipeline ,
width : usize ,
height : usize ,
frame_step_us : u64 ,
}
2026-05-17 00:42:08 -03:00
#[ derive(Clone, Copy, Debug, Default) ]
struct EncodeStats {
frames : u64 ,
total_bytes : u128 ,
min_bytes : usize ,
max_bytes : usize ,
oversize_frames : u64 ,
}
impl EncodeStats {
fn record ( & mut self , bytes : usize , max_frame_bytes : usize ) {
self . frames = self . frames . saturating_add ( 1 ) ;
self . total_bytes = self . total_bytes . saturating_add ( bytes as u128 ) ;
self . min_bytes = if self . frames = = 1 {
bytes
} else {
self . min_bytes . min ( bytes )
} ;
self . max_bytes = self . max_bytes . max ( bytes ) ;
if max_frame_bytes > 0 & & bytes > max_frame_bytes {
self . oversize_frames = self . oversize_frames . saturating_add ( 1 ) ;
}
}
fn mean_bytes ( & self ) -> usize {
if self . frames = = 0 {
0
} else {
( self . total_bytes / u128 ::from ( self . frames ) ) . min ( usize ::MAX as u128 ) as usize
}
}
}
2026-05-16 06:48:01 -03:00
impl MjpegEncoder {
fn new ( args : & Args ) -> Result < Self > {
gst ::init ( ) . context ( " gst init " ) ? ;
let width = args . width as i32 ;
let height = args . height as i32 ;
let fps = args . fps as i32 ;
let raw_caps = gst ::Caps ::builder ( " video/x-raw " )
. field ( " format " , " RGB " )
. field ( " width " , width )
. field ( " height " , height )
. field ( " framerate " , gst ::Fraction ::new ( fps , 1 ) )
. build ( ) ;
let jpeg_caps = gst ::Caps ::builder ( " image/jpeg " )
. field ( " parsed " , true )
. field ( " width " , width )
. field ( " height " , height )
. field ( " framerate " , gst ::Fraction ::new ( fps , 1 ) )
. build ( ) ;
let pipeline = gst ::Pipeline ::new ( ) ;
let src = gst ::ElementFactory ::make ( " appsrc " )
. name ( " lesavka_synthetic_uplink_src " )
. build ( ) ?
. downcast ::< gst_app ::AppSrc > ( )
. expect ( " appsrc " ) ;
src . set_is_live ( false ) ;
src . set_format ( gst ::Format ::Time ) ;
src . set_property ( " do-timestamp " , false ) ;
src . set_caps ( Some ( & raw_caps ) ) ;
let convert = gst ::ElementFactory ::make ( " videoconvert " ) . build ( ) ? ;
let encoder = gst ::ElementFactory ::make ( " jpegenc " )
. property ( " quality " , args . jpeg_quality )
. build ( ) ? ;
let capsfilter = gst ::ElementFactory ::make ( " capsfilter " )
. property ( " caps " , & jpeg_caps )
. build ( ) ? ;
let sink = gst ::ElementFactory ::make ( " appsink " )
. name ( " lesavka_synthetic_uplink_sink " )
. property ( " sync " , false )
. property ( " emit-signals " , false )
. property ( " max-buffers " , 8 u32 )
. build ( ) ?
. downcast ::< gst_app ::AppSink > ( )
. expect ( " appsink " ) ;
pipeline . add_many ( [
src . upcast_ref ( ) ,
& convert ,
& encoder ,
& capsfilter ,
sink . upcast_ref ( ) ,
] ) ? ;
gst ::Element ::link_many ( [
src . upcast_ref ( ) ,
& convert ,
& encoder ,
& capsfilter ,
sink . upcast_ref ( ) ,
] ) ? ;
pipeline
. set_state ( gst ::State ::Playing )
. context ( " starting synthetic MJPEG encoder " ) ? ;
Ok ( Self {
src ,
sink ,
pipeline ,
width : args . width ,
height : args . height ,
frame_step_us : args . frame_step_us ( ) ,
} )
}
fn encode ( & mut self , sequence : u64 ) -> Result < Vec < u8 > > {
let pts_us = sequence . saturating_mul ( self . frame_step_us ) ;
let mut buffer =
gst ::Buffer ::from_slice ( synthetic_rgb_frame ( self . width , self . height , sequence ) ) ;
if let Some ( meta ) = buffer . get_mut ( ) {
let pts = gst ::ClockTime ::from_useconds ( pts_us ) ;
meta . set_pts ( Some ( pts ) ) ;
meta . set_dts ( Some ( pts ) ) ;
meta . set_duration ( Some ( gst ::ClockTime ::from_useconds ( self . frame_step_us ) ) ) ;
}
self . src
. push_buffer ( buffer )
. context ( " encoding synthetic frame " ) ? ;
let sample = self
. sink
. pull_sample ( )
. context ( " pulling encoded synthetic frame " ) ? ;
let buffer = sample
. buffer ( )
. context ( " encoded synthetic frame had no buffer " ) ? ;
let map = buffer
. map_readable ( )
. context ( " mapping encoded synthetic frame " ) ? ;
Ok ( map . as_slice ( ) . to_vec ( ) )
}
}
impl Drop for MjpegEncoder {
fn drop ( & mut self ) {
let _ = self . src . end_of_stream ( ) ;
let _ = self . pipeline . set_state ( gst ::State ::Null ) ;
}
}
2026-05-16 22:03:12 -03:00
async fn connect_channel ( args : & Args ) -> Result < Channel > {
let mut endpoint =
tonic ::transport ::Channel ::from_shared ( args . server . clone ( ) ) ? . tcp_nodelay ( true ) ;
if is_https ( & args . server ) {
endpoint = endpoint
. tls_config ( client_tls_config ( args ) ? )
. context ( " configuring synthetic uplink TLS " ) ? ;
}
endpoint
. connect ( )
. await
. with_context ( | | format! ( " connecting to {} " , args . server ) )
}
fn client_tls_config ( args : & Args ) -> Result < ClientTlsConfig > {
let mut tls = ClientTlsConfig ::new ( ) . domain_name (
args . tls_domain
. clone ( )
. or_else ( | | host_from_uri ( & args . server ) )
. unwrap_or_else ( | | " lesavka-server " . to_string ( ) ) ,
) ;
let ca_path = args
. tls_ca
. as_ref ( )
. context ( " https synthetic uplink requires --tls-ca or LESAVKA_TLS_CA " ) ? ;
let cert_path = args
. tls_client_cert
. as_ref ( )
. context ( " https synthetic uplink requires --tls-client-cert or LESAVKA_TLS_CLIENT_CERT " ) ? ;
let key_path = args
. tls_client_key
. as_ref ( )
. context ( " https synthetic uplink requires --tls-client-key or LESAVKA_TLS_CLIENT_KEY " ) ? ;
let ca = std ::fs ::read ( ca_path )
. with_context ( | | format! ( " reading TLS CA certificate {} " , ca_path . display ( ) ) ) ? ;
tls = tls . ca_certificate ( Certificate ::from_pem ( ca ) ) ;
let cert = std ::fs ::read ( cert_path )
. with_context ( | | format! ( " reading TLS client certificate {} " , cert_path . display ( ) ) ) ? ;
let key = std ::fs ::read ( key_path )
. with_context ( | | format! ( " reading TLS client key {} " , key_path . display ( ) ) ) ? ;
Ok ( tls . identity ( Identity ::from_pem ( cert , key ) ) )
}
fn is_https ( server : & str ) -> bool {
server . trim_start ( ) . starts_with ( " https:// " )
}
fn host_from_uri ( server : & str ) -> Option < String > {
let rest = server . split_once ( " :// " ) ? . 1 ;
let host_port = rest . split ( '/' ) . next ( ) . unwrap_or ( rest ) ;
let host = host_port
. rsplit_once ( '@' )
. map ( | ( _ , host ) | host )
. unwrap_or ( host_port ) ;
if host . starts_with ( '[' ) {
return host
. split_once ( ']' )
. map ( | ( value , _ ) | value . trim_start_matches ( '[' ) . to_string ( ) ) ;
}
Some ( host . split ( ':' ) . next ( ) . unwrap_or ( host ) . to_string ( ) ) . filter ( | host | ! host . is_empty ( ) )
}
fn env_path ( name : & str ) -> Option < PathBuf > {
std ::env ::var_os ( name )
. filter ( | value | ! value . is_empty ( ) )
. map ( PathBuf ::from )
}
fn default_pki_path ( file_name : & str ) -> Option < PathBuf > {
let home = std ::env ::var_os ( " HOME " ) ? ;
Some (
PathBuf ::from ( home )
. join ( " .config " )
. join ( " lesavka " )
. join ( " pki " )
. join ( file_name ) ,
)
}
2026-05-16 06:48:01 -03:00
#[ tokio::main(flavor = " current_thread " ) ]
async fn main ( ) -> Result < ( ) > {
let args = Args ::parse ( ) ? ;
if let Some ( dir ) = & args . artifact_dir {
std ::fs ::create_dir_all ( dir ) . with_context ( | | format! ( " creating {} " , dir . display ( ) ) ) ? ;
std ::fs ::write (
dir . join ( " command.txt " ) ,
std ::env ::args ( ) . collect ::< Vec < _ > > ( ) . join ( " " ) + " \n " ,
) ? ;
2026-05-17 00:42:08 -03:00
write_summary ( & args , None ) ? ;
2026-05-16 06:48:01 -03:00
}
2026-05-16 22:03:12 -03:00
let channel = connect_channel ( & args ) . await ? ;
2026-05-16 06:48:01 -03:00
let mut client = RelayClient ::new ( channel ) ;
let ( tx , rx ) = mpsc ::channel ::< UpstreamMediaBundle > ( 8 ) ;
let response_task = tokio ::spawn ( async move {
let response = client
. stream_webcam_media ( Request ::new ( ReceiverStream ::new ( rx ) ) )
. await
. context ( " opening StreamWebcamMedia " ) ? ;
let mut inbound = response . into_inner ( ) ;
while inbound
. message ( )
. await
. context ( " reading StreamWebcamMedia response " ) ?
. is_some ( )
{ }
Ok ::< ( ) , anyhow ::Error > ( ( ) )
} ) ;
let mut encoder = MjpegEncoder ::new ( & args ) ? ;
2026-05-17 00:42:08 -03:00
let mut encode_stats = EncodeStats ::default ( ) ;
2026-05-16 06:48:01 -03:00
let frame_step = Duration ::from_micros ( args . frame_step_us ( ) ) ;
let started = tokio ::time ::Instant ::now ( ) + Duration ::from_millis ( 250 ) ;
let total_frames = args . total_frames ( ) ;
eprintln! (
" lesavka synthetic uplink: mode={}x{}@{} frames={} server={} session={} " ,
args . width , args . height , args . fps , total_frames , args . server , args . session_id
) ;
for sequence in 0 .. total_frames {
tokio ::time ::sleep_until ( started + duration_mul ( frame_step , sequence ) ) . await ;
let pts_us = sequence . saturating_mul ( args . frame_step_us ( ) ) ;
let encoded = encoder . encode ( sequence ) ? ;
2026-05-17 00:42:08 -03:00
encode_stats . record ( encoded . len ( ) , args . max_frame_bytes ) ;
if args . max_frame_bytes > 0 & & encoded . len ( ) > args . max_frame_bytes {
write_summary ( & args , Some ( & encode_stats ) ) ? ;
bail! (
" encoded synthetic frame {sequence} is {} bytes, above --max-frame-bytes {}; lower --jpeg-quality or use a more compressible synthetic pattern before trusting the RCT probe " ,
encoded . len ( ) ,
args . max_frame_bytes
) ;
}
2026-05-16 06:48:01 -03:00
let bundle = synthetic_bundle ( & args , sequence , pts_us , encoded ) ;
2026-05-16 20:58:24 -03:00
if tx . send ( bundle ) . await . is_err ( ) {
let response_result = response_task
. await
. context ( " joining StreamWebcamMedia task after early close " ) ? ;
match response_result {
Ok ( ( ) ) = > bail! (
" StreamWebcamMedia closed before accepting synthetic frame {sequence}; disconnect or pause any live Lesavka client upstream before running the isolated RCT probe "
) ,
Err ( err ) = > {
return Err ( err )
. context ( " StreamWebcamMedia closed before accepting synthetic frame " ) ;
}
}
}
2026-05-16 06:48:01 -03:00
if args . print_every > 0 & & sequence > 0 & & sequence % args . print_every = = 0 {
eprintln! ( " sent synthetic frame {sequence} / {total_frames} " ) ;
}
}
drop ( tx ) ;
response_task
. await
. context ( " joining StreamWebcamMedia task " ) ? ? ;
2026-05-17 00:42:08 -03:00
write_summary ( & args , Some ( & encode_stats ) ) ? ;
2026-05-16 06:48:01 -03:00
eprintln! ( " lesavka synthetic uplink complete: frames= {total_frames} " ) ;
Ok ( ( ) )
}
fn synthetic_bundle ( args : & Args , sequence : u64 , pts_us : u64 , data : Vec < u8 > ) -> UpstreamMediaBundle {
let video = VideoPacket {
id : 0 ,
pts : pts_us ,
data ,
seq : sequence ,
effective_fps : args . fps ,
client_capture_pts_us : pts_us ,
client_send_pts_us : pts_us ,
.. Default ::default ( )
} ;
let audio = AudioPacket {
id : 0 ,
pts : pts_us ,
data : silence_pcm ( args . frame_step_us ( ) ) ,
seq : sequence ,
client_capture_pts_us : pts_us ,
client_send_pts_us : pts_us ,
encoding : AudioEncoding ::PcmS16le as i32 ,
sample_rate : DEFAULT_SAMPLE_RATE ,
channels : DEFAULT_CHANNELS ,
frame_duration_us : args . frame_step_us ( ) . min ( u64 ::from ( u32 ::MAX ) ) as u32 ,
.. Default ::default ( )
} ;
UpstreamMediaBundle {
session_id : args . session_id ,
seq : sequence ,
capture_start_us : pts_us ,
capture_end_us : pts_us ,
video : Some ( video ) ,
audio : vec ! [ audio ] ,
audio_sample_rate : DEFAULT_SAMPLE_RATE ,
audio_channels : DEFAULT_CHANNELS ,
video_width : args . width as u32 ,
video_height : args . height as u32 ,
video_fps : args . fps ,
audio_encoding : AudioEncoding ::PcmS16le as i32 ,
}
}
fn silence_pcm ( duration_us : u64 ) -> Vec < u8 > {
let samples = ( u64 ::from ( DEFAULT_SAMPLE_RATE ) . saturating_mul ( duration_us ) / 1_000_000 ) . max ( 1 ) ;
let bytes = samples
. saturating_mul ( u64 ::from ( DEFAULT_CHANNELS ) )
. saturating_mul ( std ::mem ::size_of ::< i16 > ( ) as u64 )
. min ( usize ::MAX as u64 ) as usize ;
vec! [ 0 ; bytes ]
}
fn synthetic_rgb_frame ( width : usize , height : usize , sequence : u64 ) -> Vec < u8 > {
let mut frame = vec! [ 0 u8 ; width . saturating_mul ( height ) . saturating_mul ( 3 ) ] ;
2026-05-17 00:42:08 -03:00
let moving_width = ( width / 10 ) . max ( 32 ) . min ( width . max ( 1 ) ) ;
let moving_offset = ( sequence as usize ) . wrapping_mul ( 13 ) % width . max ( 1 ) ;
2026-05-16 06:48:01 -03:00
for y in 0 .. height {
for x in 0 .. width {
2026-05-17 00:42:08 -03:00
let value = synthetic_luma (
2026-05-16 17:47:58 -03:00
( width , height ) ,
2026-05-16 06:48:01 -03:00
x ,
y ,
sequence ,
2026-05-17 00:42:08 -03:00
( moving_width , moving_offset ) ,
2026-05-16 06:48:01 -03:00
) ;
let offset = ( y * width + x ) * 3 ;
frame [ offset ] = value ;
frame [ offset + 1 ] = value ;
frame [ offset + 2 ] = value ;
}
}
draw_sequence_marker ( & mut frame , width , height , sequence ) ;
frame
}
fn synthetic_luma (
2026-05-16 17:47:58 -03:00
frame_size : ( usize , usize ) ,
2026-05-16 06:48:01 -03:00
x : usize ,
y : usize ,
sequence : u64 ,
2026-05-17 00:42:08 -03:00
moving_bar : ( usize , usize ) ,
2026-05-16 06:48:01 -03:00
) -> u8 {
2026-05-16 17:47:58 -03:00
let ( width , height ) = frame_size ;
2026-05-17 00:42:08 -03:00
let ( moving_width , moving_offset ) = moving_bar ;
let width = width . max ( 1 ) ;
let height = height . max ( 1 ) ;
let block_w = ( width / 24 ) . max ( 24 ) ;
let block_h = ( height / 18 ) . max ( 18 ) ;
let base = 44
+ ( x . saturating_mul ( 72 ) / width )
+ ( y . saturating_mul ( 52 ) / height )
+ ( ( sequence as usize ) . saturating_mul ( 3 ) % 28 ) ;
let checker = if ( ( ( x / block_w ) + ( y / block_h ) + ( sequence as usize / 5 ) ) & 1 ) = = 0 {
30
} else {
0
} ;
let mut value = ( base + checker ) . min ( 238 ) as u8 ;
let moving = ( x + width - moving_offset ) % width ;
2026-05-16 06:48:01 -03:00
if moving < moving_width {
2026-05-17 00:42:08 -03:00
value = ( 220 usize . saturating_sub ( y . saturating_mul ( 54 ) / height ) ) . min ( 255 ) as u8 ;
} else if moving < moving_width + 4 {
value = 28 ;
2026-05-16 06:48:01 -03:00
}
let center_x = width / 2 ;
let center_y = height / 2 ;
if x . abs_diff ( center_x ) < width / 9 & & y . abs_diff ( center_y ) < height / 12 {
value = 255 u8 . saturating_sub ( value / 2 ) ;
}
value
}
fn marker_cell ( width : usize , height : usize ) -> usize {
( width . min ( height ) / 80 ) . clamp ( 6 , 16 )
}
fn draw_sequence_marker ( frame : & mut [ u8 ] , width : usize , height : usize , sequence : u64 ) {
let cell = marker_cell ( width , height ) ;
let rows = MARKER_BITS . div_ceil ( MARKER_COLUMNS ) ;
if width < ( MARKER_COLUMNS + 4 ) * cell | | height < ( rows + 4 ) * cell {
return ;
}
let x0 = 2 * cell ;
let y0 = 2 * cell ;
fill_rect (
frame ,
width ,
cell ,
cell ,
( MARKER_COLUMNS + 2 ) * cell ,
( rows + 2 ) * cell ,
32 ,
) ;
fill_rect ( frame , width , x0 - cell , y0 - cell , cell , cell , 255 ) ;
fill_rect (
frame ,
width ,
x0 + MARKER_COLUMNS * cell ,
y0 - cell ,
cell ,
cell ,
0 ,
) ;
for bit in 0 .. MARKER_BITS {
let col = bit % MARKER_COLUMNS ;
let row = bit / MARKER_COLUMNS ;
let value = if ( ( sequence > > bit ) & 1 ) ! = 0 { 255 } else { 0 } ;
fill_rect (
frame ,
width ,
x0 + col * cell ,
y0 + row * cell ,
cell ,
cell ,
value ,
) ;
}
}
fn fill_rect ( frame : & mut [ u8 ] , width : usize , x0 : usize , y0 : usize , w : usize , h : usize , value : u8 ) {
let pixels = frame . len ( ) / 3 ;
let height = pixels / width . max ( 1 ) ;
let x1 = ( x0 + w ) . min ( width ) ;
let y1 = ( y0 + h ) . min ( height ) ;
for y in y0 .. y1 {
for x in x0 .. x1 {
let offset = ( y * width + x ) * 3 ;
if let Some ( pixel ) = frame . get_mut ( offset .. offset + 3 ) {
pixel [ 0 ] = value ;
pixel [ 1 ] = value ;
pixel [ 2 ] = value ;
}
}
}
}
fn parse_mode ( value : & str ) -> Result < ( usize , usize , u32 ) > {
let ( size , fps ) = value
. split_once ( '@' )
. with_context ( | | format! ( " mode must look like WIDTHxHEIGHT@FPS, got {value:?} " ) ) ? ;
let ( width , height ) = size
. split_once ( 'x' )
. with_context ( | | format! ( " mode must look like WIDTHxHEIGHT@FPS, got {value:?} " ) ) ? ;
Ok ( ( width . parse ( ) ? , height . parse ( ) ? , fps . parse ( ) ? ) )
}
fn next_value ( it : & mut impl Iterator < Item = String > , flag : & str ) -> Result < String > {
it . next ( )
. with_context ( | | format! ( " {flag} requires a value " ) )
}
fn parse_next < T > ( it : & mut impl Iterator < Item = String > , flag : & str ) -> Result < T >
where
T : std ::str ::FromStr ,
T ::Err : std ::error ::Error + Send + Sync + 'static ,
{
Ok ( next_value ( it , flag ) ? . parse ( ) ? )
}
2026-05-17 00:42:08 -03:00
fn env_usize ( name : & str ) -> Option < usize > {
std ::env ::var ( name )
. ok ( )
. and_then ( | value | value . trim ( ) . parse ::< usize > ( ) . ok ( ) )
}
2026-05-16 06:48:01 -03:00
fn duration_mul ( duration : Duration , count : u64 ) -> Duration {
Duration ::from_nanos (
duration
. as_nanos ( )
. saturating_mul ( u128 ::from ( count ) )
. min ( u128 ::from ( u64 ::MAX ) ) as u64 ,
)
}
fn unix_millis ( ) -> u64 {
std ::time ::SystemTime ::now ( )
. duration_since ( std ::time ::UNIX_EPOCH )
. unwrap_or_default ( )
. as_millis ( )
. min ( u128 ::from ( u64 ::MAX ) ) as u64
}
2026-05-17 00:42:08 -03:00
fn write_summary ( args : & Args , stats : Option < & EncodeStats > ) -> Result < ( ) > {
if let Some ( dir ) = & args . artifact_dir {
std ::fs ::write (
dir . join ( " summary.json " ) ,
args_summary_json ( args , stats ) + " \n " ,
) ? ;
}
Ok ( ( ) )
}
fn json_usize_or_null ( value : Option < usize > ) -> String {
value
. map ( | value | value . to_string ( ) )
. unwrap_or_else ( | | " null " . to_string ( ) )
}
fn args_summary_json ( args : & Args , stats : Option < & EncodeStats > ) -> String {
let frames = stats . map ( | stats | stats . frames ) . unwrap_or ( 0 ) ;
let min_bytes =
json_usize_or_null ( stats . and_then ( | stats | ( stats . frames > 0 ) . then_some ( stats . min_bytes ) ) ) ;
let max_bytes =
json_usize_or_null ( stats . and_then ( | stats | ( stats . frames > 0 ) . then_some ( stats . max_bytes ) ) ) ;
let mean_bytes = json_usize_or_null (
stats . and_then ( | stats | ( stats . frames > 0 ) . then_some ( stats . mean_bytes ( ) ) ) ,
) ;
let oversize_frames = stats . map ( | stats | stats . oversize_frames ) . unwrap_or ( 0 ) ;
2026-05-16 06:48:01 -03:00
format! (
2026-05-17 00:42:08 -03:00
" {{ \" schema \" : \" lesavka.synthetic-uplink.v1 \" , \" server \" :{server:?}, \" width \" :{width}, \" height \" :{height}, \" fps \" :{fps}, \" duration_s \" :{duration:.3}, \" session_id \" :{session}, \" tls \" :{tls}, \" jpeg_quality \" :{quality}, \" max_frame_bytes \" :{max_frame_bytes}, \" encoded_frames \" :{frames}, \" encoded_min_bytes \" :{min_bytes}, \" encoded_max_bytes \" :{max_bytes}, \" encoded_mean_bytes \" :{mean_bytes}, \" encoded_oversize_frames \" :{oversize_frames}}} " ,
2026-05-16 06:48:01 -03:00
server = args . server ,
width = args . width ,
height = args . height ,
fps = args . fps ,
duration = args . duration . as_secs_f64 ( ) ,
session = args . session_id ,
2026-05-16 22:03:12 -03:00
tls = is_https ( & args . server ) ,
2026-05-17 00:42:08 -03:00
quality = args . jpeg_quality ,
max_frame_bytes = args . max_frame_bytes ,
frames = frames ,
min_bytes = min_bytes ,
max_bytes = max_bytes ,
mean_bytes = mean_bytes ,
oversize_frames = oversize_frames ,
2026-05-16 06:48:01 -03:00
)
}
fn print_help ( ) {
println! (
" lesavka-synthetic-uplink \n \n \
Sends sequence - coded synthetic MJPEG plus silent PCM through StreamWebcamMedia . \ n \ n \
Options :\ n \
- - server URL gRPC endpoint , default { DEFAULT_SERVER } \ n \
- - mode WIDTHxHEIGHT @ FPS shorthand for width / height / fps \ n \
- - width N - - height N - - fps N \ n \
- - duration SECONDS default 300 \ n \
- - jpeg - quality N default { DEFAULT_JPEG_QUALITY } \ n \
2026-05-17 00:42:08 -03:00
- - max - frame - bytes N fail fast if an encoded frame exceeds N bytes \ n \
2026-05-16 06:48:01 -03:00
- - artifact - dir PATH write command / summary metadata \ n \
- - print - every N progress interval in frames "
) ;
}
2026-05-17 00:42:08 -03:00
#[ cfg(test) ]
mod tests {
use super ::* ;
fn test_args ( width : usize , height : usize , fps : u32 ) -> Args {
Args {
server : DEFAULT_SERVER . to_string ( ) ,
width ,
height ,
fps ,
duration : Duration ::from_secs ( 1 ) ,
jpeg_quality : DEFAULT_JPEG_QUALITY ,
session_id : 1 ,
artifact_dir : None ,
print_every : 0 ,
max_frame_bytes : 232_106 ,
tls_ca : None ,
tls_client_cert : None ,
tls_client_key : None ,
tls_domain : None ,
}
}
#[ test ]
fn synthetic_frames_fit_safe_720p_and_1080p_isochronous_budget ( ) {
for ( width , height , fps ) in [ ( 1280 , 720 , 30 ) , ( 1920 , 1080 , 30 ) ] {
let args = test_args ( width , height , fps ) ;
let mut encoder = MjpegEncoder ::new ( & args ) . expect ( " synthetic encoder " ) ;
for sequence in [ 0 , 1 , 30 , 120 , 300 ] {
let encoded = encoder . encode ( sequence ) . expect ( " encode frame " ) ;
assert! (
encoded . len ( ) < = args . max_frame_bytes ,
" {}x{}@{} synthetic frame {sequence} encoded to {} bytes, above {} " ,
width ,
height ,
fps ,
encoded . len ( ) ,
args . max_frame_bytes
) ;
}
}
}
}