0.1
This commit is contained in:
165
src/media_audio.rs
Normal file
165
src/media_audio.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
// 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<std::path::PathBuf, AppError> {
|
||||
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-gst-record");
|
||||
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<std::path::PathBuf, AppError> {
|
||||
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<gst::Pipeline, AppError> {
|
||||
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::<gst::Pipeline>();
|
||||
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<String, AppError> {
|
||||
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<String, AppError> {
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user