v0.2.0 working

This commit is contained in:
2026-03-30 23:11:57 +02:00
parent d3197b9603
commit 65d8d8d3f6
15 changed files with 403 additions and 77 deletions

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "tauri-video02" name = "tauri-video02"
version = "0.1.1" version = "0.2.0"
description = "A Tauri Video App" description = "A Tauri Video App"
authors = ["sinus@sasedev.net"] authors = ["sinus@sasedev.net"]
edition = "2024" edition = "2024"
@@ -29,3 +29,4 @@ tauri = { version = "^2.10", features = ["default"] }
tauri-plugin-opener = { version = "^2.5", features = [] } tauri-plugin-opener = { version = "^2.5", features = [] }
tracing = { version = "^0.1", features = ["async-await", "log"] } tracing = { version = "^0.1", features = ["async-await", "log"] }
tracing-subscriber = { version = "^0.3", features = ["ansi", "env-filter", "chrono", "serde", "json"] } tracing-subscriber = { version = "^0.3", features = ["ansi", "env-filter", "chrono", "serde", "json"] }
ts-rs = { version = "^12.0", features = [] }

View File

@@ -14,15 +14,22 @@
<h1>POC 2 - Recording + Preview</h1> <h1>POC 2 - Recording + Preview</h1>
<section class="card"> <section class="card">
<h2>Audio</h2> <h2>Video Preview</h2>
<div class="preview-wrap">
<img id="video-preview" alt="Video preview" />
</div>
</section>
<section class="card">
<h2>Audio + Video</h2>
<div class="actions"> <div class="actions">
<button id="start-audio-btn" type="button">Start audio</button> <button id="start-av-btn" type="button">Start AV</button>
<button id="stop-audio-btn" type="button" disabled>Stop <button id="stop-av-btn" type="button" disabled>Stop AV</button>
audio</button>
</div> </div>
<pre id="audio-status">Ready.</pre> <pre id="av-status">Ready.</pre>
</section> </section>
<section class="card"> <section class="card">
@@ -34,11 +41,19 @@
video</button> video</button>
</div> </div>
<div class="preview-wrap"> <pre id="video-status">Ready.</pre>
<img id="video-preview" alt="Video preview" /> </section>
<section class="card">
<h2>Audio</h2>
<div class="actions">
<button id="start-audio-btn" type="button">Start audio</button>
<button id="stop-audio-btn" type="button" disabled>Stop
audio</button>
</div> </div>
<pre id="video-status">Ready.</pre> <pre id="audio-status">Ready.</pre>
</section> </section>
</main> </main>
<script type="module" src="main.ts" defer></script> <script type="module" src="main.ts" defer></script>

View File

@@ -1,4 +1,9 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { AvStartResponse } from './ts/bindings/AvStartResponse';
import { AvStopResponse } from './ts/bindings/AvStopResponse';
type Mode = "idle" | "audio" | "video" | "av";
const startAudioBtn = document.querySelector<HTMLButtonElement>("#start-audio-btn"); const startAudioBtn = document.querySelector<HTMLButtonElement>("#start-audio-btn");
const stopAudioBtn = document.querySelector<HTMLButtonElement>("#stop-audio-btn"); const stopAudioBtn = document.querySelector<HTMLButtonElement>("#stop-audio-btn");
@@ -7,7 +12,12 @@ const audioStatus = document.querySelector<HTMLElement>("#audio-status");
const startVideoBtn = document.querySelector<HTMLButtonElement>("#start-video-btn"); const startVideoBtn = document.querySelector<HTMLButtonElement>("#start-video-btn");
const stopVideoBtn = document.querySelector<HTMLButtonElement>("#stop-video-btn"); const stopVideoBtn = document.querySelector<HTMLButtonElement>("#stop-video-btn");
const videoStatus = document.querySelector<HTMLElement>("#video-status"); const videoStatus = document.querySelector<HTMLElement>("#video-status");
const videoPreview = document.querySelector<HTMLImageElement>("#video-preview");
const startAvBtn = document.querySelector<HTMLButtonElement>("#start-av-btn");
const stopAvBtn = document.querySelector<HTMLButtonElement>("#stop-av-btn");
const avStatus = document.querySelector<HTMLElement>("#av-status");
const previewElement = document.querySelector<HTMLImageElement>("#video-preview");
if ( if (
startAudioBtn === null || startAudioBtn === null ||
@@ -16,19 +26,34 @@ if (
startVideoBtn === null || startVideoBtn === null ||
stopVideoBtn === null || stopVideoBtn === null ||
videoStatus === null || videoStatus === null ||
videoPreview === null startAvBtn === null ||
stopAvBtn === null ||
avStatus === null ||
previewElement === null
) { ) {
throw new Error("missing UI elements"); throw new Error("missing UI elements");
} }
let currentMode: Mode = "idle";
let previewTimer: number | null = null; let previewTimer: number | null = null;
let previewRequestInFlight = false; let previewRequestInFlight = false;
let previewObjectUrl: string | null = null;
function setAudioStatus(message: string): void { function setAudioStatus(message: string): void {
if (audioStatus) if (audioStatus)
audioStatus.textContent = message; audioStatus.textContent = message;
} }
function setVideoStatus(message: string): void {
if (videoStatus)
videoStatus.textContent = message;
}
function setAvStatus(message: string): void {
if (avStatus)
avStatus.textContent = message;
}
function setAudioButtons(isRecording: boolean): void { function setAudioButtons(isRecording: boolean): void {
if (startAudioBtn) if (startAudioBtn)
startAudioBtn.disabled = isRecording; startAudioBtn.disabled = isRecording;
@@ -36,11 +61,6 @@ function setAudioButtons(isRecording: boolean): void {
stopAudioBtn.disabled = !isRecording; stopAudioBtn.disabled = !isRecording;
} }
function setVideoStatus(message: string): void {
if (videoStatus)
videoStatus.textContent = message;
}
function setVideoButtons(isRecording: boolean): void { function setVideoButtons(isRecording: boolean): void {
if (startVideoBtn) if (startVideoBtn)
startVideoBtn.disabled = isRecording; startVideoBtn.disabled = isRecording;
@@ -48,11 +68,92 @@ function setVideoButtons(isRecording: boolean): void {
stopVideoBtn.disabled = !isRecording; stopVideoBtn.disabled = !isRecording;
} }
function setAvButtons(isRecording: boolean): void {
if (startAvBtn)
startAvBtn.disabled = isRecording;
if (stopAvBtn)
stopAvBtn.disabled = !isRecording;
}
function setAllButtonsForMode(mode: Mode): void {
if (mode === "idle") {
setAudioButtons(false);
setVideoButtons(false);
setAvButtons(false);
return;
}
setAudioButtons(mode === "audio");
setVideoButtons(mode === "video");
setAvButtons(mode === "av");
if (mode !== "audio") {
if (stopAudioBtn)
stopAudioBtn.disabled = true;
if (startAudioBtn)
startAudioBtn.disabled = true;
}
if (mode !== "video") {
if (stopVideoBtn)
stopVideoBtn.disabled = true;
if (startVideoBtn)
startVideoBtn.disabled = true;
}
if (mode !== "av") {
if (stopAvBtn)
stopAvBtn.disabled = true;
if (startAvBtn)
startAvBtn.disabled = true;
}
}
function setCurrentMode(mode: Mode): void {
currentMode = mode;
setAllButtonsForMode(mode);
}
function base64ToUint8Array(base64: string): Uint8Array {
const binary = window.atob(base64);
const bytes = new Uint8Array(binary.length);
let index = 0;
while (index < binary.length) {
bytes[index] = binary.charCodeAt(index);
index += 1;
}
return bytes;
}
function updatePreviewImageFromBase64(encoded: string): void {
const bytes = base64ToUint8Array(encoded);
const copied = new Uint8Array(bytes.byteLength);
copied.set(bytes);
const blob = new Blob([copied.buffer], { type: "image/jpeg" });
const objectUrl = URL.createObjectURL(blob);
if (previewObjectUrl !== null) {
URL.revokeObjectURL(previewObjectUrl);
}
previewObjectUrl = objectUrl;
if (previewElement)
previewElement.src = objectUrl;
}
async function refreshPreviewFrame(): Promise<void> { async function refreshPreviewFrame(): Promise<void> {
if (previewRequestInFlight) { if (previewRequestInFlight) {
return; return;
} }
if (currentMode !== "video" && currentMode !== "av") {
return;
}
previewRequestInFlight = true; previewRequestInFlight = true;
try { try {
@@ -73,8 +174,6 @@ function startPreviewPolling(): void {
return; return;
} }
console.log("starting preview polling");
window.setTimeout(() => { window.setTimeout(() => {
void refreshPreviewFrame(); void refreshPreviewFrame();
}, 120); }, 120);
@@ -91,97 +190,133 @@ function stopPreviewPolling(): void {
} }
previewRequestInFlight = false; previewRequestInFlight = false;
if (videoPreview)
videoPreview.removeAttribute("src");
if (previewObjectUrl !== null) { if (previewObjectUrl !== null) {
URL.revokeObjectURL(previewObjectUrl); URL.revokeObjectURL(previewObjectUrl);
previewObjectUrl = null; previewObjectUrl = null;
} }
if (previewElement)
previewElement.removeAttribute("src");
} }
startAudioBtn.addEventListener("click", async () => { startAudioBtn.addEventListener("click", async () => {
startAudioBtn.disabled = true; if (currentMode !== "idle") {
setAudioStatus("Another mode is already running.");
return;
}
setCurrentMode("audio");
try { try {
const path = await invoke<string>("start_audio_recording"); const path = await invoke<string>("start_audio_recording");
setAudioStatus(`Audio recording started.\nOutput: ${path}`); setAudioStatus(`Audio recording started.\nOutput: ${path}`);
setAudioButtons(true);
} catch (error) { } catch (error) {
setAudioStatus(`Start audio failed.\n${String(error)}`); setAudioStatus(`Start audio failed.\n${String(error)}`);
setAudioButtons(false); setCurrentMode("idle");
} }
}); });
stopAudioBtn.addEventListener("click", async () => { stopAudioBtn.addEventListener("click", async () => {
if (currentMode !== "audio") {
setAudioStatus("Audio mode is not running.");
return;
}
stopAudioBtn.disabled = true; stopAudioBtn.disabled = true;
try { try {
const path = await invoke<string>("stop_audio_recording"); const path = await invoke<string>("stop_audio_recording");
setAudioStatus(`Audio recording stopped.\nSaved file: ${path}`); setAudioStatus(`Audio recording stopped.\nSaved file: ${path}`);
setAudioButtons(false); setCurrentMode("idle");
} catch (error) { } catch (error) {
setAudioStatus(`Stop audio failed.\n${String(error)}`); setAudioStatus(`Stop audio failed.\n${String(error)}`);
setAudioButtons(true); setCurrentMode("audio");
} }
}); });
startVideoBtn.addEventListener("click", async () => { startVideoBtn.addEventListener("click", async () => {
startVideoBtn.disabled = true; if (currentMode !== "idle") {
setVideoStatus("Another mode is already running.");
return;
}
setCurrentMode("video");
try { try {
const path = await invoke<string>("start_video_recording"); const path = await invoke<string>("start_video_recording");
setVideoStatus(`Video recording started.\nOutput: ${path}`); setVideoStatus(`Video recording started.\nOutput: ${path}`);
setVideoButtons(true);
startPreviewPolling(); startPreviewPolling();
} catch (error) { } catch (error) {
setVideoStatus(`Start video failed.\n${String(error)}`); setVideoStatus(`Start video failed.\n${String(error)}`);
setVideoButtons(false);
stopPreviewPolling(); stopPreviewPolling();
setCurrentMode("idle");
} }
}); });
stopVideoBtn.addEventListener("click", async () => { stopVideoBtn.addEventListener("click", async () => {
if (currentMode !== "video") {
setVideoStatus("Video mode is not running.");
return;
}
stopVideoBtn.disabled = true; stopVideoBtn.disabled = true;
try { try {
const path = await invoke<string>("stop_video_recording"); const path = await invoke<string>("stop_video_recording");
setVideoStatus(`Video recording stopped.\nSaved file: ${path}`); setVideoStatus(`Video recording stopped.\nSaved file: ${path}`);
setVideoButtons(false);
stopPreviewPolling(); stopPreviewPolling();
setCurrentMode("idle");
} catch (error) { } catch (error) {
setVideoStatus(`Stop video failed.\n${String(error)}`); setVideoStatus(`Stop video failed.\n${String(error)}`);
setVideoButtons(true); setCurrentMode("video");
} }
}); });
let previewObjectUrl: string | null = null; startAvBtn.addEventListener("click", async () => {
if (currentMode !== "idle") {
function base64ToUint8Array(base64: string): Uint8Array { setAvStatus("Another mode is already running.");
const binary = window.atob(base64); return;
const bytes = new Uint8Array(binary.length);
for (let index = 0;index < binary.length;index += 1) {
bytes[index] = binary.charCodeAt(index);
} }
return bytes; setCurrentMode("av");
}
function updatePreviewImageFromBase64(encoded: string): void { try {
const bytes = base64ToUint8Array(encoded); const result = await invoke<AvStartResponse>("start_av_recording");
const copied = new Uint8Array(bytes.byteLength);
copied.set(bytes); setAvStatus(
`AV recording started.\nAudio: ${result.audio_path}\nVideo: ${result.video_path}`
);
const blob = new Blob([copied.buffer], { type: "image/jpeg" }); startPreviewPolling();
const objectUrl = URL.createObjectURL(blob); } catch (error) {
setAvStatus(`Start AV failed.\n${String(error)}`);
stopPreviewPolling();
setCurrentMode("idle");
}
});
if (previewObjectUrl !== null) { stopAvBtn.addEventListener("click", async () => {
URL.revokeObjectURL(previewObjectUrl); if (currentMode !== "av") {
setAvStatus("AV mode is not running.");
return;
} }
previewObjectUrl = objectUrl; stopAvBtn.disabled = true;
if (videoPreview)
videoPreview.src = objectUrl; try {
} const result = await invoke<AvStopResponse>("stop_av_recording");
setAvStatus(
`AV recording stopped.\nAudio: ${result.audio_path}\nVideo: ${result.video_path}`
);
stopPreviewPolling();
setCurrentMode("idle");
} catch (error) {
setAvStatus(`Stop AV failed.\n${String(error)}`);
setCurrentMode("av");
}
});
setCurrentMode("idle");

View File

@@ -27,6 +27,7 @@ body {
display: flex; display: flex;
gap: 12px; gap: 12px;
margin-bottom: 16px; margin-bottom: 16px;
flex-wrap: wrap;
} }
button { button {

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AvStartResponse = { audio_path: string, video_path: string, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AvStopResponse = { audio_path: string, video_path: string, };

View File

@@ -1,7 +1,7 @@
{ {
"name": "tauri-video02", "name": "tauri-video02",
"private": true, "private": true,
"version": "0.1.1", "version": "0.2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -44,10 +44,23 @@ impl PreviewState {
} }
} }
pub struct AvRecorderState {
pub is_recording: bool,
}
impl AvRecorderState {
pub fn new() -> Self {
Self {
is_recording: false,
}
}
}
pub struct AppState { pub struct AppState {
pub audio: std::sync::Mutex<AudioRecorderState>, pub audio: std::sync::Mutex<AudioRecorderState>,
pub video: std::sync::Mutex<VideoRecorderState>, pub video: std::sync::Mutex<VideoRecorderState>,
pub preview: PreviewState, pub preview: PreviewState,
pub av: std::sync::Mutex<AvRecorderState>,
} }
impl AppState { impl AppState {
@@ -56,6 +69,7 @@ impl AppState {
audio: std::sync::Mutex::new(AudioRecorderState::new()), audio: std::sync::Mutex::new(AudioRecorderState::new()),
video: std::sync::Mutex::new(VideoRecorderState::new()), video: std::sync::Mutex::new(VideoRecorderState::new()),
preview: PreviewState::new(), preview: PreviewState::new(),
av: std::sync::Mutex::new(AvRecorderState::new()),
} }
} }
} }

View File

@@ -2,6 +2,7 @@
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::media_audio; use crate::media_audio;
use crate::media_av;
use crate::media_video; use crate::media_video;
#[tauri::command] #[tauri::command]
@@ -50,3 +51,25 @@ pub async fn get_video_preview_frame_base64(
Err(error) => Err(error.to_user_message()), Err(error) => Err(error.to_user_message()),
} }
} }
#[tauri::command]
pub async fn start_av_recording(
state: tauri::State<'_, AppState>,
) -> Result<media_av::AvStartResponse, String> {
let result = media_av::start_av_recording(&state);
match result {
Ok(value) => Ok(value),
Err(error) => Err(error.to_user_message()),
}
}
#[tauri::command]
pub async fn stop_av_recording(
state: tauri::State<'_, AppState>,
) -> Result<media_av::AvStopResponse, String> {
let result = media_av::stop_av_recording(&state);
match result {
Ok(value) => Ok(value),
Err(error) => Err(error.to_user_message()),
}
}

View File

@@ -4,6 +4,7 @@ mod app_state;
mod commands; mod commands;
mod error; mod error;
mod media_audio; mod media_audio;
mod media_av;
mod media_video; mod media_video;
fn init_tracing() { fn init_tracing() {
@@ -40,6 +41,8 @@ pub fn run() {
commands::start_video_recording, commands::start_video_recording,
commands::stop_video_recording, commands::stop_video_recording,
commands::get_video_preview_frame_base64, commands::get_video_preview_frame_base64,
commands::start_av_recording,
commands::stop_av_recording,
]); ]);
let run_result = builder.run(tauri::generate_context!()); let run_result = builder.run(tauri::generate_context!());

View File

@@ -2,14 +2,16 @@
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::error::AppError; use crate::error::AppError;
use gstreamer as gst;
use gst::prelude::*; use gst::prelude::*;
use gstreamer as gst;
fn build_output_dir() -> Result<std::path::PathBuf, AppError> { fn build_output_dir() -> Result<std::path::PathBuf, AppError> {
let app_data_dir = dirs::data_local_dir(); let app_data_dir = dirs::data_local_dir();
let Some(mut base_dir) = app_data_dir else { let Some(mut base_dir) = app_data_dir else {
return Err(AppError::Io("unable to resolve local data directory".to_string())); return Err(AppError::Io(
"unable to resolve local data directory".to_string(),
));
}; };
base_dir.push("tauri-video02"); base_dir.push("tauri-video02");
@@ -27,16 +29,26 @@ fn build_output_dir() -> Result<std::path::PathBuf, AppError> {
} }
fn build_output_path() -> Result<std::path::PathBuf, AppError> { fn build_output_path() -> Result<std::path::PathBuf, AppError> {
let mut dir = build_output_dir()?; let build_dir_result = build_output_dir();
let mut dir = match build_dir_result {
Ok(value) => value,
Err(error) => return Err(error),
};
let timestamp = chrono::Local::now().format("%Y%m%d-%H%M%S").to_string(); let timestamp = chrono::Local::now().format("%Y%m%d-%H%M%S").to_string();
dir.push(format!("audio-{timestamp}.wav")); dir.push(format!("audio-{timestamp}.wav"));
Ok(dir) Ok(dir)
} }
fn build_pipeline(output_path: &std::path::Path) -> Result<gst::Pipeline, AppError> { fn build_pipeline(output_path: &std::path::Path) -> Result<gst::Pipeline, AppError> {
let location = output_path.to_string_lossy().replace('\\', "\\\\").replace('"', "\\\""); let location = output_path
.to_string_lossy()
.replace('\\', "\\\\")
.replace('"', "\\\"");
let pipeline_str = format!( let pipeline_str = format!(
"autoaudiosrc ! audioconvert ! audioresample ! wavenc ! filesink location=\"{location}\"" "autoaudiosrc ! audioconvert ! audioresample ! wavenc ! filesink location=\"{}\"",
location
); );
tracing::info!(pipeline = %pipeline_str, "building audio pipeline"); tracing::info!(pipeline = %pipeline_str, "building audio pipeline");
@@ -45,9 +57,7 @@ fn build_pipeline(output_path: &std::path::Path) -> Result<gst::Pipeline, AppErr
let element = match element_result { let element = match element_result {
Ok(value) => value, Ok(value) => value,
Err(error) => { Err(error) => {
return Err(AppError::Gst(format!( return Err(AppError::Gst(format!("unable to parse pipeline: {error}")));
"unable to parse pipeline: {error}"
)));
} }
}; };
@@ -64,9 +74,7 @@ pub fn start_audio_recording(state: &tauri::State<'_, AppState>) -> Result<Strin
let mut guard = match state.audio.lock() { let mut guard = match state.audio.lock() {
Ok(value) => value, Ok(value) => value,
Err(_) => { Err(_) => {
return Err(AppError::State( return Err(AppError::State("audio state lock poisoned".to_string()));
"audio state lock poisoned".to_string(),
));
} }
}; };
@@ -76,8 +84,15 @@ pub fn start_audio_recording(state: &tauri::State<'_, AppState>) -> Result<Strin
)); ));
} }
let output_path = build_output_path()?; let output_path = match build_output_path() {
let pipeline = build_pipeline(&output_path)?; Ok(value) => value,
Err(error) => return Err(error),
};
let pipeline = match build_pipeline(&output_path) {
Ok(value) => value,
Err(error) => return Err(error),
};
let set_state_result = pipeline.set_state(gst::State::Playing); let set_state_result = pipeline.set_state(gst::State::Playing);
match set_state_result { match set_state_result {
@@ -102,9 +117,7 @@ pub fn stop_audio_recording(state: &tauri::State<'_, AppState>) -> Result<String
let mut guard = match state.audio.lock() { let mut guard = match state.audio.lock() {
Ok(value) => value, Ok(value) => value,
Err(_) => { Err(_) => {
return Err(AppError::State( return Err(AppError::State("audio state lock poisoned".to_string()));
"audio state lock poisoned".to_string(),
));
} }
}; };
@@ -134,7 +147,7 @@ pub fn stop_audio_recording(state: &tauri::State<'_, AppState>) -> Result<String
let send_event_result = pipeline.send_event(gst::event::Eos::new()); let send_event_result = pipeline.send_event(gst::event::Eos::new());
if !send_event_result { if !send_event_result {
tracing::warn!("failed to send EOS event to pipeline"); tracing::warn!("failed to send EOS event to audio pipeline");
} }
let bus = pipeline.bus(); let bus = pipeline.bus();

120
src/media_av.rs Normal file
View File

@@ -0,0 +1,120 @@
// file: src/media_av.rs
use crate::app_state::AppState;
use crate::error::AppError;
use crate::media_audio;
use crate::media_video;
#[derive(serde::Serialize, ts_rs::TS)]
#[ts(export, export_to = "../frontend/ts/bindings/")]
pub struct AvStartResponse {
pub audio_path: String,
pub video_path: String,
}
#[derive(serde::Serialize, ts_rs::TS)]
#[ts(export, export_to = "../frontend/ts/bindings/")]
pub struct AvStopResponse {
pub audio_path: String,
pub video_path: String,
}
pub fn start_av_recording(state: &tauri::State<'_, AppState>) -> Result<AvStartResponse, AppError> {
let mut av_guard = match state.av.lock() {
Ok(value) => value,
Err(_) => {
return Err(AppError::State("av state lock poisoned".to_string()));
}
};
if av_guard.is_recording {
return Err(AppError::State(
"av recording is already running".to_string(),
));
}
let audio_guard_result = state.audio.lock();
match audio_guard_result {
Ok(audio_guard) => {
if audio_guard.is_recording {
return Err(AppError::State("audio already running".to_string()));
}
}
Err(_) => {
return Err(AppError::State("audio state lock poisoned".to_string()));
}
}
let video_guard_result = state.video.lock();
match video_guard_result {
Ok(video_guard) => {
if video_guard.is_recording {
return Err(AppError::State("video already running".to_string()));
}
}
Err(_) => {
return Err(AppError::State("video state lock poisoned".to_string()));
}
}
let audio_start_result = media_audio::start_audio_recording(state);
let audio_path = match audio_start_result {
Ok(value) => value,
Err(error) => return Err(error),
};
let video_start_result = media_video::start_video_recording(state);
let video_path = match video_start_result {
Ok(value) => value,
Err(error) => {
let rollback_result = media_audio::stop_audio_recording(state);
if let Err(rollback_error) = rollback_result {
tracing::warn!(
error = %rollback_error.to_user_message(),
"failed to rollback audio after video start failure"
);
}
return Err(error);
}
};
av_guard.is_recording = true;
Ok(AvStartResponse {
audio_path,
video_path,
})
}
pub fn stop_av_recording(state: &tauri::State<'_, AppState>) -> Result<AvStopResponse, AppError> {
let mut av_guard = match state.av.lock() {
Ok(value) => value,
Err(_) => {
return Err(AppError::State("av state lock poisoned".to_string()));
}
};
if !av_guard.is_recording {
return Err(AppError::State("av recording is not running".to_string()));
}
let audio_stop_result = media_audio::stop_audio_recording(state);
let audio_path = match audio_stop_result {
Ok(value) => value,
Err(error) => return Err(error),
};
let video_stop_result = media_video::stop_video_recording(state);
let video_path = match video_stop_result {
Ok(value) => value,
Err(error) => return Err(error),
};
av_guard.is_recording = false;
Ok(AvStopResponse {
audio_path,
video_path,
})
}

View File

@@ -123,8 +123,6 @@ fn attach_preview_callbacks(
let bytes = map.as_slice().to_vec(); let bytes = map.as_slice().to_vec();
tracing::info!(size = bytes.len(), "preview sample received");
let lock_result = preview_store.lock(); let lock_result = preview_store.lock();
match lock_result { match lock_result {
Ok(mut guard) => { Ok(mut guard) => {
@@ -286,12 +284,9 @@ pub fn get_video_preview_frame_base64(
}; };
let Some(bytes) = guard.as_ref() else { let Some(bytes) = guard.as_ref() else {
tracing::info!("preview frame requested but none is available yet");
return Ok(None); return Ok(None);
}; };
tracing::info!(size = bytes.len(), "preview frame requested and returned");
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes); let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
Ok(Some(encoded)) Ok(Some(encoded))
} }

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "tauri-video02", "productName": "tauri-video02",
"version": "0.1.1", "version": "0.2.0",
"identifier": "com.sinus.tauri-video02", "identifier": "com.sinus.tauri-video02",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",