Skip to content

Commit 50b72ed

Browse files
Merge pull request #1716 from CapSoftware/hls-recording-reliability
segmented MP4 upload pipeline with live HLS playback and server-side muxing
2 parents 9cd8437 + ba83b96 commit 50b72ed

File tree

52 files changed

+6272
-572
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+6272
-572
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ unnecessary_lazy_evaluations = "deny"
9494
needless_range_loop = "deny"
9595
manual_clamp = "deny"
9696

97+
[profile.dev]
98+
debug = 1
99+
100+
[profile.dev.package."*"]
101+
debug = 0
102+
97103
# Optimize for smaller binary size
98104
[profile.release]
99105
panic = "unwind"

apps/desktop/src-tauri/src/api.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,34 @@ pub struct Organization {
265265
pub owner_id: String,
266266
}
267267

268+
pub async fn signal_recording_complete(
269+
app: &AppHandle,
270+
video_id: &str,
271+
) -> Result<(), AuthedApiError> {
272+
let resp = app
273+
.authed_api_request("/api/upload/recording-complete", |client, url| {
274+
client
275+
.post(url)
276+
.header("Content-Type", "application/json")
277+
.json(&serde_json::json!({
278+
"videoId": video_id,
279+
}))
280+
})
281+
.await
282+
.map_err(|err| format!("api/signal_recording_complete/request: {err}"))?;
283+
284+
if !resp.status().is_success() {
285+
let status = resp.status().as_u16();
286+
let error_body = resp
287+
.text()
288+
.await
289+
.unwrap_or_else(|_| "<no response body>".to_string());
290+
return Err(format!("api/signal_recording_complete/{status}: {error_body}").into());
291+
}
292+
293+
Ok(())
294+
}
295+
268296
pub async fn fetch_organizations(app: &AppHandle) -> Result<Vec<Organization>, AuthedApiError> {
269297
let resp = app
270298
.authed_api_request("/api/desktop/organizations", |client, url| client.get(url))

apps/desktop/src-tauri/src/camera.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ impl CameraPreviewManager {
266266
})
267267
.ok();
268268

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

271271
shutdown_complete_tx.send(()).ok();
272272
info!("DONE");
@@ -276,6 +276,12 @@ impl CameraPreviewManager {
276276
}
277277
}
278278

279+
fn wait_for_shutdown_signal(runtime: &Runtime, receiver: oneshot::Receiver<()>, timeout: Duration) {
280+
runtime.block_on(async move {
281+
let _ = tokio::time::timeout(timeout, receiver).await;
282+
});
283+
}
284+
279285
// Internal events for the persistent camera renderer architecture.
280286
//
281287
// The camera preview uses a persistent WGPU renderer that stays alive across
@@ -1025,7 +1031,9 @@ async fn resize_window(
10251031

10261032
#[cfg(test)]
10271033
mod tests {
1028-
use super::preferred_alpha_mode;
1034+
use super::{preferred_alpha_mode, wait_for_shutdown_signal};
1035+
use std::thread;
1036+
use tokio::{runtime::Runtime, sync::oneshot, time::Duration};
10291037
use wgpu::CompositeAlphaMode;
10301038

10311039
#[test]
@@ -1066,6 +1074,19 @@ mod tests {
10661074
);
10671075
assert_eq!(preferred_alpha_mode(&[]), CompositeAlphaMode::Opaque);
10681076
}
1077+
1078+
#[test]
1079+
fn wait_for_shutdown_signal_can_run_from_plain_thread() {
1080+
let runtime = Runtime::new().unwrap();
1081+
let (tx, rx) = oneshot::channel();
1082+
1083+
let handle = thread::spawn(move || {
1084+
wait_for_shutdown_signal(&runtime, rx, Duration::from_millis(100));
1085+
});
1086+
1087+
tx.send(()).ok();
1088+
handle.join().unwrap();
1089+
}
10691090
}
10701091

10711092
fn render_solid_frame(color: [u8; 4], width: u32, height: u32) -> (Vec<u8>, u32) {
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use tokio::task::JoinHandle;
2+
3+
pub(crate) fn run_while_active<T, FExit, F>(is_exiting: FExit, operation: F) -> Option<T>
4+
where
5+
FExit: Fn() -> bool,
6+
F: FnOnce() -> T,
7+
{
8+
if is_exiting() {
9+
None
10+
} else {
11+
Some(operation())
12+
}
13+
}
14+
15+
pub(crate) fn collect_device_inventory<TCamera, TMicrophone, FExit, FCamera, FMicrophone>(
16+
is_exiting: FExit,
17+
camera_permitted: bool,
18+
microphone_permitted: bool,
19+
list_cameras: FCamera,
20+
list_microphones: FMicrophone,
21+
) -> Option<(Vec<TCamera>, Vec<TMicrophone>)>
22+
where
23+
FExit: Fn() -> bool,
24+
FCamera: FnOnce() -> Vec<TCamera>,
25+
FMicrophone: FnOnce() -> Vec<TMicrophone>,
26+
{
27+
if is_exiting() {
28+
return None;
29+
}
30+
31+
let cameras = if camera_permitted {
32+
if is_exiting() {
33+
return None;
34+
}
35+
36+
list_cameras()
37+
} else {
38+
Vec::new()
39+
};
40+
41+
if is_exiting() {
42+
return None;
43+
}
44+
45+
let microphones = if microphone_permitted {
46+
if is_exiting() {
47+
return None;
48+
}
49+
50+
list_microphones()
51+
} else {
52+
Vec::new()
53+
};
54+
55+
if is_exiting() {
56+
return None;
57+
}
58+
59+
Some((cameras, microphones))
60+
}
61+
62+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63+
pub(crate) enum AppExitAction {
64+
#[cfg(target_os = "macos")]
65+
Process(i32),
66+
#[cfg(not(target_os = "macos"))]
67+
Runtime(i32),
68+
}
69+
70+
pub(crate) fn app_exit_action(exit_code: i32) -> AppExitAction {
71+
#[cfg(target_os = "macos")]
72+
{
73+
AppExitAction::Process(exit_code)
74+
}
75+
76+
#[cfg(not(target_os = "macos"))]
77+
{
78+
AppExitAction::Runtime(exit_code)
79+
}
80+
}
81+
82+
pub(crate) fn read_target_under_cursor<TDisplay, TWindow, FExit, FDisplay, FWindow>(
83+
is_exiting: FExit,
84+
display: FDisplay,
85+
window: FWindow,
86+
) -> Option<(Option<TDisplay>, Option<TWindow>)>
87+
where
88+
FExit: Fn() -> bool,
89+
FDisplay: FnOnce() -> Option<TDisplay>,
90+
FWindow: FnOnce() -> Option<TWindow>,
91+
{
92+
if is_exiting() {
93+
return None;
94+
}
95+
96+
let display = display();
97+
98+
if is_exiting() {
99+
return None;
100+
}
101+
102+
let window = window();
103+
104+
if is_exiting() {
105+
return None;
106+
}
107+
108+
Some((display, window))
109+
}
110+
111+
pub(crate) fn abort_join_handles<T>(
112+
tasks: impl IntoIterator<Item = JoinHandle<T>>,
113+
task: Option<JoinHandle<T>>,
114+
) {
115+
for task in tasks {
116+
task.abort();
117+
}
118+
119+
if let Some(task) = task {
120+
task.abort();
121+
}
122+
}

0 commit comments

Comments
 (0)