// file: src/media_audio.rs use crate::app_state::AppState; use crate::error::AppError; use gstreamer as gst; use gst::prelude::*; fn build_output_dir() -> Result { let app_data_dir = dirs::data_local_dir(); let Some(mut base_dir) = app_data_dir else { return Err(AppError::Io("unable to resolve local data directory".to_string())); }; base_dir.push("tauri-video01"); base_dir.push("records"); let create_dir_result = std::fs::create_dir_all(&base_dir); if let Err(error) = create_dir_result { return Err(AppError::Io(format!( "unable to create output directory '{}': {error}", base_dir.display() ))); } Ok(base_dir) } fn build_output_path() -> Result { let mut dir = build_output_dir()?; let timestamp = chrono::Local::now().format("%Y%m%d-%H%M%S").to_string(); dir.push(format!("audio-{timestamp}.wav")); Ok(dir) } fn build_pipeline(output_path: &std::path::Path) -> Result { let location = output_path.to_string_lossy().replace('\\', "\\\\").replace('"', "\\\""); let pipeline_str = format!( "autoaudiosrc ! audioconvert ! audioresample ! wavenc ! filesink location=\"{location}\"" ); tracing::info!(pipeline = %pipeline_str, "building audio pipeline"); let element_result = gst::parse::launch(&pipeline_str); let element = match element_result { Ok(value) => value, Err(error) => { return Err(AppError::Gst(format!( "unable to parse pipeline: {error}" ))); } }; let downcast_result = element.downcast::(); match downcast_result { Ok(pipeline) => Ok(pipeline), Err(_) => Err(AppError::Gst( "parsed element is not a pipeline".to_string(), )), } } pub fn start_audio_recording(state: &tauri::State<'_, AppState>) -> Result { let mut guard = match state.audio.lock() { Ok(value) => value, Err(_) => { return Err(AppError::State( "audio state lock poisoned".to_string(), )); } }; if guard.is_recording { return Err(AppError::State( "audio recording is already running".to_string(), )); } let output_path = build_output_path()?; let pipeline = build_pipeline(&output_path)?; let set_state_result = pipeline.set_state(gst::State::Playing); match set_state_result { Ok(_) => {} Err(error) => { return Err(AppError::Gst(format!( "unable to set pipeline to playing: {error:?}" ))); } } tracing::info!(path = %output_path.display(), "audio recording started"); guard.is_recording = true; guard.output_path = Some(output_path.clone()); guard.pipeline = Some(pipeline); Ok(output_path.display().to_string()) } pub fn stop_audio_recording(state: &tauri::State<'_, AppState>) -> Result { let mut guard = match state.audio.lock() { Ok(value) => value, Err(_) => { return Err(AppError::State( "audio state lock poisoned".to_string(), )); } }; if !guard.is_recording { return Err(AppError::State( "audio recording is not running".to_string(), )); } let pipeline = match guard.pipeline.take() { Some(value) => value, None => { return Err(AppError::State( "missing pipeline while recording flag is set".to_string(), )); } }; let output_path = match guard.output_path.clone() { Some(value) => value, None => { return Err(AppError::State( "missing output path while recording flag is set".to_string(), )); } }; let send_event_result = pipeline.send_event(gst::event::Eos::new()); if !send_event_result { tracing::warn!("failed to send EOS event to pipeline"); } let bus = pipeline.bus(); if let Some(bus_ref) = bus { let _ = bus_ref.timed_pop_filtered( gst::ClockTime::from_seconds(5), &[gst::MessageType::Eos, gst::MessageType::Error], ); } let set_state_result = pipeline.set_state(gst::State::Null); match set_state_result { Ok(_) => {} Err(error) => { return Err(AppError::Gst(format!( "unable to set pipeline to null: {error:?}" ))); } } tracing::info!(path = %output_path.display(), "audio recording stopped"); guard.is_recording = false; guard.output_path = None; guard.pipeline = None; Ok(output_path.display().to_string()) }