Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
daf73e4
build(cargo): add dev profile debug settings
richiemcilroy Apr 7, 2026
4dbbb71
build(setup): add sccache detection and cache native deps
richiemcilroy Apr 7, 2026
c32412a
build(docker): switch media-server to host network mode
richiemcilroy Apr 7, 2026
e4cbb55
feat(enc-ffmpeg): add segment event types and video encoder callback …
richiemcilroy Apr 7, 2026
cc28cbd
feat(enc-ffmpeg): add DASH audio segment encoder
richiemcilroy Apr 7, 2026
083a54f
feat(enc-ffmpeg): add merge_video_audio remux utility
richiemcilroy Apr 7, 2026
e820efc
feat(enc-avfoundation): skip AV drift throttling in instant mode
richiemcilroy Apr 7, 2026
a0190db
feat(recording): refactor instant pipeline to segmented video output
richiemcilroy Apr 7, 2026
9c6c0ef
test(recording): add hardware and segmented pipeline tests
richiemcilroy Apr 7, 2026
1b0fc5c
feat(project): add SegmentUpload variant to UploadMeta
richiemcilroy Apr 7, 2026
08955d6
feat(desktop): add exit shutdown module and app exit guards
richiemcilroy Apr 7, 2026
4689641
feat(desktop): add segment upload pipeline and recording integration
richiemcilroy Apr 7, 2026
edd12d1
feat(database): add desktopSegments video source type
richiemcilroy Apr 7, 2026
f481cb1
feat(web-domain): add SegmentsSource and segment manifest types
richiemcilroy Apr 7, 2026
dd38328
feat(web-backend): add continuationToken to S3 listObjects
richiemcilroy Apr 7, 2026
a216d32
feat(web): add desktopSegments recording mode and segments playlist
richiemcilroy Apr 7, 2026
205fbcc
feat(web): add recording-complete endpoint for segment muxing
richiemcilroy Apr 7, 2026
23ecc94
feat(media-server): add webhook secret auth and mux-segments endpoint
richiemcilroy Apr 7, 2026
5d0b15c
feat(web): pass webhook secret and handle segment source migration
richiemcilroy Apr 7, 2026
7bed650
feat(web): add live segments HLS playback support
richiemcilroy Apr 7, 2026
6fc387a
refactor(web): extract media player selectors to module scope
richiemcilroy Apr 7, 2026
0e617c4
chore: update pnpm lockfile
richiemcilroy Apr 7, 2026
9fb870c
queries
richiemcilroy Apr 7, 2026
a19b4f5
Use formatted console.log in media-server webhook
richiemcilroy Apr 7, 2026
ef473c7
Add explicit types for cues, subtitles, renditions
richiemcilroy Apr 7, 2026
af3aab9
clippy
richiemcilroy Apr 7, 2026
ba83b96
fmt
richiemcilroy Apr 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ unnecessary_lazy_evaluations = "deny"
needless_range_loop = "deny"
manual_clamp = "deny"

[profile.dev]
debug = 1

[profile.dev.package."*"]
debug = 0

# Optimize for smaller binary size
[profile.release]
panic = "unwind"
Expand Down
28 changes: 28 additions & 0 deletions apps/desktop/src-tauri/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,34 @@ pub struct Organization {
pub owner_id: String,
}

pub async fn signal_recording_complete(
app: &AppHandle,
video_id: &str,
) -> Result<(), AuthedApiError> {
let resp = app
.authed_api_request("/api/upload/recording-complete", |client, url| {
client
.post(url)
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"videoId": video_id,
}))
})
.await
.map_err(|err| format!("api/signal_recording_complete/request: {err}"))?;

if !resp.status().is_success() {
let status = resp.status().as_u16();
let error_body = resp
.text()
.await
.unwrap_or_else(|_| "<no response body>".to_string());
return Err(format!("api/signal_recording_complete/{status}: {error_body}").into());
}

Ok(())
}

pub async fn fetch_organizations(app: &AppHandle) -> Result<Vec<Organization>, AuthedApiError> {
let resp = app
.authed_api_request("/api/desktop/organizations", |client, url| client.get(url))
Expand Down
25 changes: 23 additions & 2 deletions apps/desktop/src-tauri/src/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ impl CameraPreviewManager {
})
.ok();

let _ = rt.block_on(tokio::time::timeout(Duration::from_millis(250), drop_rx));
wait_for_shutdown_signal(&rt, drop_rx, Duration::from_millis(250));

shutdown_complete_tx.send(()).ok();
info!("DONE");
Expand All @@ -276,6 +276,12 @@ impl CameraPreviewManager {
}
}

fn wait_for_shutdown_signal(runtime: &Runtime, receiver: oneshot::Receiver<()>, timeout: Duration) {
runtime.block_on(async move {
let _ = tokio::time::timeout(timeout, receiver).await;
});
}

// Internal events for the persistent camera renderer architecture.
//
// The camera preview uses a persistent WGPU renderer that stays alive across
Expand Down Expand Up @@ -1025,7 +1031,9 @@ async fn resize_window(

#[cfg(test)]
mod tests {
use super::preferred_alpha_mode;
use super::{preferred_alpha_mode, wait_for_shutdown_signal};
use std::thread;
use tokio::{runtime::Runtime, sync::oneshot, time::Duration};
use wgpu::CompositeAlphaMode;

#[test]
Expand Down Expand Up @@ -1066,6 +1074,19 @@ mod tests {
);
assert_eq!(preferred_alpha_mode(&[]), CompositeAlphaMode::Opaque);
}

#[test]
fn wait_for_shutdown_signal_can_run_from_plain_thread() {
let runtime = Runtime::new().unwrap();
let (tx, rx) = oneshot::channel();

let handle = thread::spawn(move || {
wait_for_shutdown_signal(&runtime, rx, Duration::from_millis(100));
});

tx.send(()).ok();
handle.join().unwrap();
}
}

fn render_solid_frame(color: [u8; 4], width: u32, height: u32) -> (Vec<u8>, u32) {
Expand Down
122 changes: 122 additions & 0 deletions apps/desktop/src-tauri/src/exit_shutdown.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use tokio::task::JoinHandle;

pub(crate) fn run_while_active<T, FExit, F>(is_exiting: FExit, operation: F) -> Option<T>
where
FExit: Fn() -> bool,
F: FnOnce() -> T,
{
if is_exiting() {
None
} else {
Some(operation())
}
}

pub(crate) fn collect_device_inventory<TCamera, TMicrophone, FExit, FCamera, FMicrophone>(
is_exiting: FExit,
camera_permitted: bool,
microphone_permitted: bool,
list_cameras: FCamera,
list_microphones: FMicrophone,
) -> Option<(Vec<TCamera>, Vec<TMicrophone>)>
where
FExit: Fn() -> bool,
FCamera: FnOnce() -> Vec<TCamera>,
FMicrophone: FnOnce() -> Vec<TMicrophone>,
{
if is_exiting() {
return None;
}

let cameras = if camera_permitted {
if is_exiting() {
return None;
}

list_cameras()
} else {
Vec::new()
};

if is_exiting() {
return None;
}

let microphones = if microphone_permitted {
if is_exiting() {
return None;
}

list_microphones()
} else {
Vec::new()
};

if is_exiting() {
return None;
}

Some((cameras, microphones))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AppExitAction {
#[cfg(target_os = "macos")]
Process(i32),
#[cfg(not(target_os = "macos"))]
Runtime(i32),
}

pub(crate) fn app_exit_action(exit_code: i32) -> AppExitAction {
#[cfg(target_os = "macos")]
{
AppExitAction::Process(exit_code)
}

#[cfg(not(target_os = "macos"))]
{
AppExitAction::Runtime(exit_code)
}
}

pub(crate) fn read_target_under_cursor<TDisplay, TWindow, FExit, FDisplay, FWindow>(
is_exiting: FExit,
display: FDisplay,
window: FWindow,
) -> Option<(Option<TDisplay>, Option<TWindow>)>
where
FExit: Fn() -> bool,
FDisplay: FnOnce() -> Option<TDisplay>,
FWindow: FnOnce() -> Option<TWindow>,
{
if is_exiting() {
return None;
}

let display = display();

if is_exiting() {
return None;
}

let window = window();

if is_exiting() {
return None;
}

Some((display, window))
}

pub(crate) fn abort_join_handles<T>(
tasks: impl IntoIterator<Item = JoinHandle<T>>,
task: Option<JoinHandle<T>>,
) {
for task in tasks {
task.abort();
}

if let Some(task) = task {
task.abort();
}
}
Loading
Loading