nandi/jolt-nativepublic Fork 0
3e8c6f0
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Run the formatter over the tree

`just lint` has always run `cargo fmt --all --check`, but nothing enforced it:
the old pipeline only built the RBE image buck2's executor pulled. CI is
`nix flake check` now, which runs the formatter, so the drift that accumulated
in thirteen files has to come out first.

Formatting only — no behaviour changes. It does cost something real: av.rs,
av_media.rs, v4l2cam.rs and android_camera.rs are kept close to sleek's copies
so a fix can be moved between the two by eye, and they have moved a little
further away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-05T23:00:39-07:00 Browse files
3e8c6f0 parent: d682bfa
modified .gitignore +4 -0
@@ -18,3 +18,7 @@
1818 # writes for an editor to find.
1919 **/.jolt/cpcache/
2020 **/.nrepl-port
21+
22+# nix build output symlinks
23+result
24+result-*
@@ -18,3 +18,7 @@
18 # writes for an editor to find.18 # writes for an editor to find.
19 **/.jolt/cpcache/19 **/.jolt/cpcache/
20 **/.nrepl-port20 **/.nrepl-port
21+
22+# nix build output symlinks
23+result
24+result-*
modified crates/jolt-abi/src/lib.rs +3 -1
@@ -131,7 +131,9 @@ mod tests {
131131 use std::ffi::CStr;
132132
133133 fn read(ptr: *const c_char) -> String {
134- unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned()
134+ unsafe { CStr::from_ptr(ptr) }
135+ .to_string_lossy()
136+ .into_owned()
135137 }
136138
137139 #[test]
@@ -131,7 +131,9 @@ mod tests {
131 use std::ffi::CStr;131 use std::ffi::CStr;
132 132
133 fn read(ptr: *const c_char) -> String {133 fn read(ptr: *const c_char) -> String {
134- unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned()134+ unsafe { CStr::from_ptr(ptr) }
135+ .to_string_lossy()
136+ .into_owned()
135 }137 }
136 138
137 #[test]139 #[test]
modified crates/jolt-moq/src/android_camera.rs +18 -17
@@ -330,26 +330,25 @@ pub extern "system" fn Java_uk_nandi_frq_CameraCapture_onNv12Frame<'local>(
330330 };
331331 // Apply sensor/display orientation so published + preview frames
332332 // are upright (Camera2 ImageReader buffers are sensor-oriented).
333- let (y_bytes, uv_bytes, w, h, y_str, uv_str) =
334- match crate::nv12_orient::orient_nv12(
335- &y_bytes,
336- &uv_bytes,
333+ let (y_bytes, uv_bytes, w, h, y_str, uv_str) = match crate::nv12_orient::orient_nv12(
334+ &y_bytes,
335+ &uv_bytes,
336+ src_w,
337+ src_h,
338+ y_stride as u32,
339+ uv_stride as u32,
340+ rot,
341+ ) {
342+ Some((y, uv, w, h)) => (y, uv, w, h, w, w),
343+ None => (
344+ y_bytes,
345+ uv_bytes,
337346 src_w,
338347 src_h,
339348 y_stride as u32,
340349 uv_stride as u32,
341- rot,
342- ) {
343- Some((y, uv, w, h)) => (y, uv, w, h, w, w),
344- None => (
345- y_bytes,
346- uv_bytes,
347- src_w,
348- src_h,
349- y_stride as u32,
350- uv_stride as u32,
351- ),
352- };
350+ ),
351+ };
353352 let frame = VideoFrame::new_nv12(
354353 Nv12Planes {
355354 y_data: y_bytes,
@@ -375,7 +374,9 @@ pub extern "system" fn Java_uk_nandi_frq_CameraCapture_onNv12Frame<'local>(
375374 }
376375 let n = s.frames_pushed.fetch_add(1, Ordering::Relaxed);
377376 if n == 0 {
378- log::info!("android camera: first NV12 frame {w}x{h} (src {src_w}x{src_h} rot={rot})");
377+ log::info!(
378+ "android camera: first NV12 frame {w}x{h} (src {src_w}x{src_h} rot={rot})"
379+ );
379380 }
380381 Ok(())
381382 })
@@ -330,26 +330,25 @@ pub extern "system" fn Java_uk_nandi_frq_CameraCapture_onNv12Frame<'local>(
330 };330 };
331 // Apply sensor/display orientation so published + preview frames331 // Apply sensor/display orientation so published + preview frames
332 // are upright (Camera2 ImageReader buffers are sensor-oriented).332 // are upright (Camera2 ImageReader buffers are sensor-oriented).
333- let (y_bytes, uv_bytes, w, h, y_str, uv_str) =333+ let (y_bytes, uv_bytes, w, h, y_str, uv_str) = match crate::nv12_orient::orient_nv12(
334- match crate::nv12_orient::orient_nv12(334+ &y_bytes,
335- &y_bytes,335+ &uv_bytes,
336- &uv_bytes,336+ src_w,
337+ src_h,
338+ y_stride as u32,
339+ uv_stride as u32,
340+ rot,
341+ ) {
342+ Some((y, uv, w, h)) => (y, uv, w, h, w, w),
343+ None => (
344+ y_bytes,
345+ uv_bytes,
337 src_w,346 src_w,
338 src_h,347 src_h,
339 y_stride as u32,348 y_stride as u32,
340 uv_stride as u32,349 uv_stride as u32,
341- rot,350+ ),
342- ) {351+ };
343- Some((y, uv, w, h)) => (y, uv, w, h, w, w),
344- None => (
345- y_bytes,
346- uv_bytes,
347- src_w,
348- src_h,
349- y_stride as u32,
350- uv_stride as u32,
351- ),
352- };
353 let frame = VideoFrame::new_nv12(352 let frame = VideoFrame::new_nv12(
354 Nv12Planes {353 Nv12Planes {
355 y_data: y_bytes,354 y_data: y_bytes,
@@ -375,7 +374,9 @@ pub extern "system" fn Java_uk_nandi_frq_CameraCapture_onNv12Frame<'local>(
375 }374 }
376 let n = s.frames_pushed.fetch_add(1, Ordering::Relaxed);375 let n = s.frames_pushed.fetch_add(1, Ordering::Relaxed);
377 if n == 0 {376 if n == 0 {
378- log::info!("android camera: first NV12 frame {w}x{h} (src {src_w}x{src_h} rot={rot})");377+ log::info!(
378+ "android camera: first NV12 frame {w}x{h} (src {src_w}x{src_h} rot={rot})"
379+ );
379 }380 }
380 Ok(())381 Ok(())
381 })382 })
modified crates/jolt-moq/src/av.rs +21 -16
@@ -14,7 +14,6 @@ use std::fmt;
1414 use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
1515 use std::sync::{Arc, Mutex};
1616
17-
1817 /// Whether to claim camera hardware when dialing the media plane.
1918 ///
2019 /// `permission_granted` is the Android runtime CAMERA check (pass `true` on
@@ -61,9 +60,7 @@ pub struct VideoFrameStore {
6160 impl fmt::Debug for VideoFrameStore {
6261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6362 let n = self.frames.lock().map(|g| g.len()).unwrap_or(0);
64- f.debug_struct("VideoFrameStore")
65- .field("len", &n)
66- .finish()
63+ f.debug_struct("VideoFrameStore").field("len", &n).finish()
6764 }
6865 }
6966
@@ -76,14 +73,19 @@ impl VideoFrameStore {
7673 if width == 0 || height == 0 {
7774 return;
7875 }
79- let expected = (width as usize).saturating_mul(height as usize).saturating_mul(4);
76+ let expected = (width as usize)
77+ .saturating_mul(height as usize)
78+ .saturating_mul(4);
8079 if rgba.len() != expected {
8180 return;
8281 }
8382 // Camera / decoder paths sometimes leave alpha at 0 (OBS virtual cam,
8483 // some MJPEG converters). egui then draws a fully transparent tile.
8584 let rgba = force_opaque_rgba(rgba);
86- let gen = self.next_gen.fetch_add(1, Ordering::Relaxed).wrapping_add(1);
85+ let gen = self
86+ .next_gen
87+ .fetch_add(1, Ordering::Relaxed)
88+ .wrapping_add(1);
8789 if let Ok(mut g) = self.frames.lock() {
8890 g.insert(
8991 nick.into(),
@@ -467,10 +469,7 @@ mod tests {
467469
468470 #[test]
469471 fn path_key_keeps_instance_path_nick_strips() {
470- assert_eq!(
471- path_key("01SESSION/alice~phone"),
472- "alice~phone"
473- );
472+ assert_eq!(path_key("01SESSION/alice~phone"), "alice~phone");
474473 assert_eq!(path_nick("01SESSION/alice~phone"), "alice");
475474 assert_eq!(path_key("01SESSION/bob"), "bob");
476475 assert_eq!(path_nick("bob~desk"), "bob");
@@ -525,15 +524,15 @@ mod tests {
525524 #[test]
526525 fn video_frame_store_seed_missing_from_copies_only_absent_keys() {
527526 let old = VideoFrameStore::new();
528- let rgba: Arc<[u8]> = Arc::from([10u8, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 1, 2, 3, 255]);
527+ let rgba: Arc<[u8]> = Arc::from([
528+ 10u8, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 1, 2, 3, 255,
529+ ]);
529530 old.set("eve", 2, 2, rgba.clone());
530531 old.set(LOCAL_PREVIEW_KEY, 2, 2, rgba);
531532
532533 let new = VideoFrameStore::new();
533534 // Live key already present — must not be overwritten by seed.
534- let live: Arc<[u8]> = Arc::from([
535- 9u8, 9, 9, 255, 9, 9, 9, 255, 9, 9, 9, 255, 9, 9, 9, 255,
536- ]);
535+ let live: Arc<[u8]> = Arc::from([9u8, 9, 9, 255, 9, 9, 9, 255, 9, 9, 9, 255, 9, 9, 9, 255]);
537536 new.set("eve", 2, 2, live.clone());
538537
539538 new.seed_missing_from(&old);
@@ -600,7 +599,10 @@ mod tests {
600599 let r_vals: Vec<u8> = frame.rgba.iter().step_by(4).copied().collect();
601600 let min = *r_vals.iter().min().unwrap();
602601 let max = *r_vals.iter().max().unwrap();
603- assert!(max > min, "real camera-like content must have pixel variance");
602+ assert!(
603+ max > min,
604+ "real camera-like content must have pixel variance"
605+ );
604606 for a in frame.rgba.iter().skip(3).step_by(4) {
605607 assert_eq!(*a, 255);
606608 }
@@ -683,7 +685,10 @@ mod tests {
683685
684686 #[test]
685687 fn can_dial_sfu_with_jwt_always_allowed() {
686- assert!(can_dial_sfu("wss://chat.example.com", Some("eyJhbGciOiJIUzI1NiJ9.e30.x")));
688+ assert!(can_dial_sfu(
689+ "wss://chat.example.com",
690+ Some("eyJhbGciOiJIUzI1NiJ9.e30.x")
691+ ));
687692 assert!(can_dial_sfu("https://remote.example", Some("tok")));
688693 assert!(can_dial_sfu("ws://localhost:4443", Some("tok")));
689694 assert!(can_dial_sfu("127.0.0.1", Some("tok")));
@@ -14,7 +14,6 @@ use std::fmt;
14 use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};14 use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
15 use std::sync::{Arc, Mutex};15 use std::sync::{Arc, Mutex};
16 16
17-
18 /// Whether to claim camera hardware when dialing the media plane.17 /// Whether to claim camera hardware when dialing the media plane.
19 ///18 ///
20 /// `permission_granted` is the Android runtime CAMERA check (pass `true` on19 /// `permission_granted` is the Android runtime CAMERA check (pass `true` on
@@ -61,9 +60,7 @@ pub struct VideoFrameStore {
61 impl fmt::Debug for VideoFrameStore {60 impl fmt::Debug for VideoFrameStore {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 let n = self.frames.lock().map(|g| g.len()).unwrap_or(0);62 let n = self.frames.lock().map(|g| g.len()).unwrap_or(0);
64- f.debug_struct("VideoFrameStore")63+ f.debug_struct("VideoFrameStore").field("len", &n).finish()
65- .field("len", &n)
66- .finish()
67 }64 }
68 }65 }
69 66
@@ -76,14 +73,19 @@ impl VideoFrameStore {
76 if width == 0 || height == 0 {73 if width == 0 || height == 0 {
77 return;74 return;
78 }75 }
79- let expected = (width as usize).saturating_mul(height as usize).saturating_mul(4);76+ let expected = (width as usize)
77+ .saturating_mul(height as usize)
78+ .saturating_mul(4);
80 if rgba.len() != expected {79 if rgba.len() != expected {
81 return;80 return;
82 }81 }
83 // Camera / decoder paths sometimes leave alpha at 0 (OBS virtual cam,82 // Camera / decoder paths sometimes leave alpha at 0 (OBS virtual cam,
84 // some MJPEG converters). egui then draws a fully transparent tile.83 // some MJPEG converters). egui then draws a fully transparent tile.
85 let rgba = force_opaque_rgba(rgba);84 let rgba = force_opaque_rgba(rgba);
86- let gen = self.next_gen.fetch_add(1, Ordering::Relaxed).wrapping_add(1);85+ let gen = self
86+ .next_gen
87+ .fetch_add(1, Ordering::Relaxed)
88+ .wrapping_add(1);
87 if let Ok(mut g) = self.frames.lock() {89 if let Ok(mut g) = self.frames.lock() {
88 g.insert(90 g.insert(
89 nick.into(),91 nick.into(),
@@ -467,10 +469,7 @@ mod tests {
467 469
468 #[test]470 #[test]
469 fn path_key_keeps_instance_path_nick_strips() {471 fn path_key_keeps_instance_path_nick_strips() {
470- assert_eq!(472+ assert_eq!(path_key("01SESSION/alice~phone"), "alice~phone");
471- path_key("01SESSION/alice~phone"),
472- "alice~phone"
473- );
474 assert_eq!(path_nick("01SESSION/alice~phone"), "alice");473 assert_eq!(path_nick("01SESSION/alice~phone"), "alice");
475 assert_eq!(path_key("01SESSION/bob"), "bob");474 assert_eq!(path_key("01SESSION/bob"), "bob");
476 assert_eq!(path_nick("bob~desk"), "bob");475 assert_eq!(path_nick("bob~desk"), "bob");
@@ -525,15 +524,15 @@ mod tests {
525 #[test]524 #[test]
526 fn video_frame_store_seed_missing_from_copies_only_absent_keys() {525 fn video_frame_store_seed_missing_from_copies_only_absent_keys() {
527 let old = VideoFrameStore::new();526 let old = VideoFrameStore::new();
528- let rgba: Arc<[u8]> = Arc::from([10u8, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 1, 2, 3, 255]);527+ let rgba: Arc<[u8]> = Arc::from([
528+ 10u8, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 1, 2, 3, 255,
529+ ]);
529 old.set("eve", 2, 2, rgba.clone());530 old.set("eve", 2, 2, rgba.clone());
530 old.set(LOCAL_PREVIEW_KEY, 2, 2, rgba);531 old.set(LOCAL_PREVIEW_KEY, 2, 2, rgba);
531 532
532 let new = VideoFrameStore::new();533 let new = VideoFrameStore::new();
533 // Live key already present — must not be overwritten by seed.534 // Live key already present — must not be overwritten by seed.
534- let live: Arc<[u8]> = Arc::from([535+ let live: Arc<[u8]> = Arc::from([9u8, 9, 9, 255, 9, 9, 9, 255, 9, 9, 9, 255, 9, 9, 9, 255]);
535- 9u8, 9, 9, 255, 9, 9, 9, 255, 9, 9, 9, 255, 9, 9, 9, 255,
536- ]);
537 new.set("eve", 2, 2, live.clone());536 new.set("eve", 2, 2, live.clone());
538 537
539 new.seed_missing_from(&old);538 new.seed_missing_from(&old);
@@ -600,7 +599,10 @@ mod tests {
600 let r_vals: Vec<u8> = frame.rgba.iter().step_by(4).copied().collect();599 let r_vals: Vec<u8> = frame.rgba.iter().step_by(4).copied().collect();
601 let min = *r_vals.iter().min().unwrap();600 let min = *r_vals.iter().min().unwrap();
602 let max = *r_vals.iter().max().unwrap();601 let max = *r_vals.iter().max().unwrap();
603- assert!(max > min, "real camera-like content must have pixel variance");602+ assert!(
603+ max > min,
604+ "real camera-like content must have pixel variance"
605+ );
604 for a in frame.rgba.iter().skip(3).step_by(4) {606 for a in frame.rgba.iter().skip(3).step_by(4) {
605 assert_eq!(*a, 255);607 assert_eq!(*a, 255);
606 }608 }
@@ -683,7 +685,10 @@ mod tests {
683 685
684 #[test]686 #[test]
685 fn can_dial_sfu_with_jwt_always_allowed() {687 fn can_dial_sfu_with_jwt_always_allowed() {
686- assert!(can_dial_sfu("wss://chat.example.com", Some("eyJhbGciOiJIUzI1NiJ9.e30.x")));688+ assert!(can_dial_sfu(
689+ "wss://chat.example.com",
690+ Some("eyJhbGciOiJIUzI1NiJ9.e30.x")
691+ ));
687 assert!(can_dial_sfu("https://remote.example", Some("tok")));692 assert!(can_dial_sfu("https://remote.example", Some("tok")));
688 assert!(can_dial_sfu("ws://localhost:4443", Some("tok")));693 assert!(can_dial_sfu("ws://localhost:4443", Some("tok")));
689 assert!(can_dial_sfu("127.0.0.1", Some("tok")));694 assert!(can_dial_sfu("127.0.0.1", Some("tok")));
modified crates/jolt-moq/src/av_media.rs +37 -87
@@ -168,10 +168,7 @@ pub fn sanitize_audio_device_pref(id: Option<String>) -> Option<String> {
168168 #[cfg(not(target_os = "android"))]
169169 fn preferred_audio_host() -> Option<String> {
170170 let hosts = AudioBackend::available_hosts();
171- if hosts
172- .iter()
173- .any(|h| h.eq_ignore_ascii_case("pipewire"))
174- {
171+ if hosts.iter().any(|h| h.eq_ignore_ascii_case("pipewire")) {
175172 Some("PipeWire".into())
176173 } else {
177174 None
@@ -536,10 +533,7 @@ async fn run_media(
536533 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
537534
538535 let our_broadcast = broadcast_path(&config.session_id, &config.nick, &config.instance);
539- log::info!(
540- "av-media: dialing {} as {our_broadcast}",
541- config.sfu_url
542- );
536+ log::info!("av-media: dialing {} as {our_broadcast}", config.sfu_url);
543537
544538 let mut client_config = moq_native::ClientConfig::default();
545539 client_config.tls.disable_verify = Some(true);
@@ -627,9 +621,7 @@ async fn run_media(
627621 // Warm-up: wait until the capture ring actually has energy so we
628622 // don't advertise "mic open" while still InputNotReady→silence.
629623 let peak = warm_up_mic(&mut *mic, std::time::Duration::from_millis(800));
630- log::info!(
631- "av-media: mic warm-up peak={peak:.4} (0 = capture silent / not ready)"
632- );
624+ log::info!("av-media: mic warm-up peak={peak:.4} (0 = capture silent / not ready)");
633625 if peak < 1e-5 {
634626 log::warn!(
635627 "av-media: microphone opened but capture is silent so far — \
@@ -669,9 +661,7 @@ async fn run_media(
669661 }
670662 }
671663 Err(e) => {
672- log::warn!(
673- "av-media: no microphone ({e}); publishing silence (listen-only)"
674- );
664+ log::warn!("av-media: no microphone ({e}); publishing silence (listen-only)");
675665 muted.store(true, Ordering::Relaxed);
676666 if let Err(e2) = broadcast.audio().set(
677667 MuteableSource::silence(muted.clone(), mic_level.clone()),
@@ -1201,19 +1191,14 @@ async fn tap_remote(
12011191 // Match freeq-sdk-ffi: tighter latency than the 150ms streaming default.
12021192 let policy = iroh_live::media::playout::PlaybackPolicy::default()
12031193 .with_max_latency(std::time::Duration::from_millis(60));
1204- let remote = match RemoteBroadcast::with_playback_policy(
1205- &path,
1206- broadcast_consumer,
1207- policy,
1208- )
1209- .await
1210- {
1211- Ok(r) => r,
1212- Err(e) => {
1213- log::warn!("av-media: catalog {path}: {e}");
1214- return;
1215- }
1216- };
1194+ let remote =
1195+ match RemoteBroadcast::with_playback_policy(&path, broadcast_consumer, policy).await {
1196+ Ok(r) => r,
1197+ Err(e) => {
1198+ log::warn!("av-media: catalog {path}: {e}");
1199+ return;
1200+ }
1201+ };
12171202
12181203 let audio_task = {
12191204 let remote = remote.clone();
@@ -1254,9 +1239,7 @@ async fn tap_remote(
12541239 // the independent video pipeline (tap_remote waits on
12551240 // `remote.closed()`, not this task exiting).
12561241 if consecutive_errs <= 3 || consecutive_errs % 10 == 0 {
1257- log::warn!(
1258- "av-media: audio sub {ps}: {e} (retry {consecutive_errs})"
1259- );
1242+ log::warn!("av-media: audio sub {ps}: {e} (retry {consecutive_errs})");
12601243 }
12611244 let backoff_ms = (500u64 * u64::from(consecutive_errs.min(8))).min(4_000);
12621245 tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
@@ -1395,10 +1378,7 @@ impl iroh_live::media::traits::AudioSource for SilenceSource {
13951378 self.format
13961379 }
13971380
1398- fn pop_samples(
1399- &mut self,
1400- buf: &mut [f32],
1401- ) -> anyhow::Result<Option<usize>> {
1381+ fn pop_samples(&mut self, buf: &mut [f32]) -> anyhow::Result<Option<usize>> {
14021382 for s in buf.iter_mut() {
14031383 *s = 0.0;
14041384 }
@@ -1585,11 +1565,7 @@ fn open_camera_with_fallback(
15851565
15861566 // Then non-virtual hardware (skip virtuals for auto-pick).
15871567 let mut cams = listed.clone();
1588- cams.sort_by(|a, b| {
1589- a.name
1590- .to_lowercase()
1591- .cmp(&b.name.to_lowercase())
1592- });
1568+ cams.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
15931569 for c in cams {
15941570 if is_virtual_camera(&c.name, &c.id) {
15951571 continue;
@@ -1597,9 +1573,10 @@ fn open_camera_with_fallback(
15971573 if c.supported_formats.is_empty() {
15981574 continue;
15991575 }
1600- if candidates.iter().any(|x| {
1601- x.as_deref() == Some(c.id.as_str()) || x.as_deref() == Some(c.name.as_str())
1602- }) {
1576+ if candidates
1577+ .iter()
1578+ .any(|x| x.as_deref() == Some(c.id.as_str()) || x.as_deref() == Some(c.name.as_str()))
1579+ {
16031580 continue;
16041581 }
16051582 candidates.push(Some(c.id));
@@ -1618,9 +1595,7 @@ fn open_camera_with_fallback(
16181595 // prefer virtual.
16191596 let user_wants_virtual = preferred.is_some_and(|p| is_virt(p));
16201597 if is_virtual_camera(&name, label) && !user_wants_virtual {
1621- log::warn!(
1622- "av-media: rejecting auto-opened virtual camera {name} ({label})"
1623- );
1598+ log::warn!("av-media: rejecting auto-opened virtual camera {name} ({label})");
16241599 errors.push(format!("{label}: rejected virtual {name}"));
16251600 continue;
16261601 }
@@ -1702,7 +1677,9 @@ fn open_camera_with_busy_retry(
17021677 let busy = e.to_string().to_ascii_lowercase().contains("busy");
17031678 last = Some(e);
17041679 if busy && attempt + 1 < ATTEMPTS {
1705- std::thread::sleep(std::time::Duration::from_millis(150 * (attempt + 1) as u64));
1680+ std::thread::sleep(std::time::Duration::from_millis(
1681+ 150 * (attempt + 1) as u64,
1682+ ));
17061683 continue;
17071684 }
17081685 break;
@@ -1784,9 +1761,7 @@ fn spawn_local_preview_pump(
17841761 }
17851762 }
17861763 Err(_) => {
1787- log::warn!(
1788- "av-media: local preview pump rgba_image panicked {w}x{h}"
1789- );
1764+ log::warn!("av-media: local preview pump rgba_image panicked {w}x{h}");
17901765 }
17911766 }
17921767 } else if track.is_closed() {
@@ -1831,8 +1806,7 @@ fn open_android_camera(
18311806 local_frame_count: Arc<AtomicU64>,
18321807 want_publish: bool,
18331808 ) -> Result<crate::android_camera::CameraCaptureGuard> {
1834- crate::android_camera::start_capture(camera_id)
1835- .context("CameraCapture.start")?;
1809+ crate::android_camera::start_capture(camera_id).context("CameraCapture.start")?;
18361810 // Camera2 open + session configure is async on a Java handler thread.
18371811 if let Err(e) = crate::android_camera::wait_until_opened(std::time::Duration::from_secs(5)) {
18381812 crate::android_camera::stop_capture();
@@ -1917,9 +1891,7 @@ impl VideoSource for GatedCameraSource {
19171891 self.inner.stop()
19181892 }
19191893
1920- fn pop_frame(
1921- &mut self,
1922- ) -> anyhow::Result<Option<iroh_live::media::format::VideoFrame>> {
1894+ fn pop_frame(&mut self) -> anyhow::Result<Option<iroh_live::media::format::VideoFrame>> {
19231895 // Check publish flag *before* capturing. The previous order (dqbuf then
19241896 // discard) left V4L2 STREAMON / the privacy LED on for the whole mute.
19251897 if !self.enabled.load(Ordering::Relaxed) {
@@ -2075,10 +2047,7 @@ impl iroh_live::media::traits::AudioSource for ToneSource {
20752047 self.format
20762048 }
20772049
2078- fn pop_samples(
2079- &mut self,
2080- buf: &mut [f32],
2081- ) -> anyhow::Result<Option<usize>> {
2050+ fn pop_samples(&mut self, buf: &mut [f32]) -> anyhow::Result<Option<usize>> {
20822051 let channels = self.format.channel_count.max(1) as usize;
20832052 let frames = buf.len() / channels;
20842053 let phase_inc = self.frequency / self.format.sample_rate as f32;
@@ -2170,10 +2139,7 @@ impl iroh_live::media::traits::AudioSource for MuteableSource {
21702139 self.inner.format()
21712140 }
21722141
2173- fn pop_samples(
2174- &mut self,
2175- buf: &mut [f32],
2176- ) -> anyhow::Result<Option<usize>> {
2142+ fn pop_samples(&mut self, buf: &mut [f32]) -> anyhow::Result<Option<usize>> {
21772143 let mut pre_peak = 0.0f32;
21782144 let n = match self.inner.pop_samples(buf)? {
21792145 Some(n) if n > 0 => {
@@ -2238,9 +2204,7 @@ mod tests {
22382204 use iroh_live::media::playout::PlaybackPolicy;
22392205 use iroh_live::media::publish::LocalBroadcast;
22402206 use iroh_live::media::subscribe::RemoteBroadcast;
2241- use iroh_live::media::traits::{
2242- AudioSink, AudioSinkHandle, AudioSource, AudioStreamFactory,
2243- };
2207+ use iroh_live::media::traits::{AudioSink, AudioSinkHandle, AudioSource, AudioStreamFactory};
22442208 use n0_future::boxed::BoxFuture;
22452209 use std::time::Duration;
22462210
@@ -2278,9 +2242,7 @@ mod tests {
22782242 }
22792243 let _ = cam.stop();
22802244 let Some(f) = frame else {
2281- eprintln!(
2282- "SKIP real_camera id={id:?}: no frames (OBS Virtual Camera not started?)"
2283- );
2245+ eprintln!("SKIP real_camera id={id:?}: no frames (OBS Virtual Camera not started?)");
22842246 return;
22852247 };
22862248 let (w, h) = (f.width(), f.height());
@@ -2291,9 +2253,7 @@ mod tests {
22912253 .get(0..4)
22922254 .map(|p| (p[0], p[1], p[2], p[3]))
22932255 .unwrap_or((0, 0, 0, 0));
2294- eprintln!(
2295- "camera id={id:?} {w}x{h} rgba0=({r0},{g0},{b0},{a0}) luma range {min}..{max}"
2296- );
2256+ eprintln!("camera id={id:?} {w}x{h} rgba0=({r0},{g0},{b0},{a0}) luma range {min}..{max}");
22972257 assert!(
22982258 max > min,
22992259 "real camera frame must be non-uniform (luma range {min}..{max}); \
@@ -2394,10 +2354,7 @@ mod tests {
23942354 assert_eq!(n, Some(buf.len()));
23952355 assert_eq!(pcm_rms(&buf), 0.0, "muted must be silence");
23962356 // Level still saw pre-mute energy from the tone.
2397- assert!(
2398- src.level.get() > 0.01,
2399- "mic meter should move while muted"
2400- );
2357+ assert!(src.level.get() > 0.01, "mic meter should move while muted");
24012358 }
24022359
24032360 #[test]
@@ -2561,13 +2518,10 @@ mod tests {
25612518 // Keep producer alive for the duration of the test.
25622519 let _keepalive = broadcast;
25632520
2564- let remote = RemoteBroadcast::with_playback_policy(
2565- &path,
2566- consumer,
2567- PlaybackPolicy::unmanaged(),
2568- )
2569- .await
2570- .expect("catalog from LocalBroadcast");
2521+ let remote =
2522+ RemoteBroadcast::with_playback_policy(&path, consumer, PlaybackPolicy::unmanaged())
2523+ .await
2524+ .expect("catalog from LocalBroadcast");
25712525
25722526 let (tx, rx) = std::sync::mpsc::sync_channel::<Vec<f32>>(64);
25732527 let backend = TapBackend { tx };
@@ -2796,12 +2750,8 @@ mod tests {
27962750 "video.clear must drop the source so privacy LED can go dark"
27972751 );
27982752 assert!(
2799- !store
2800- .snapshot()
2801- .iter()
2802- .any(|(k, _)| k == LOCAL_PREVIEW_KEY),
2753+ !store.snapshot().iter().any(|(k, _)| k == LOCAL_PREVIEW_KEY),
28032754 "local preview frame must be cleared on release"
28042755 );
28052756 }
2806-
28072757 }
@@ -168,10 +168,7 @@ pub fn sanitize_audio_device_pref(id: Option<String>) -> Option<String> {
168 #[cfg(not(target_os = "android"))]168 #[cfg(not(target_os = "android"))]
169 fn preferred_audio_host() -> Option<String> {169 fn preferred_audio_host() -> Option<String> {
170 let hosts = AudioBackend::available_hosts();170 let hosts = AudioBackend::available_hosts();
171- if hosts171+ if hosts.iter().any(|h| h.eq_ignore_ascii_case("pipewire")) {
172- .iter()
173- .any(|h| h.eq_ignore_ascii_case("pipewire"))
174- {
175 Some("PipeWire".into())172 Some("PipeWire".into())
176 } else {173 } else {
177 None174 None
@@ -536,10 +533,7 @@ async fn run_media(
536 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();533 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
537 534
538 let our_broadcast = broadcast_path(&config.session_id, &config.nick, &config.instance);535 let our_broadcast = broadcast_path(&config.session_id, &config.nick, &config.instance);
539- log::info!(536+ log::info!("av-media: dialing {} as {our_broadcast}", config.sfu_url);
540- "av-media: dialing {} as {our_broadcast}",
541- config.sfu_url
542- );
543 537
544 let mut client_config = moq_native::ClientConfig::default();538 let mut client_config = moq_native::ClientConfig::default();
545 client_config.tls.disable_verify = Some(true);539 client_config.tls.disable_verify = Some(true);
@@ -627,9 +621,7 @@ async fn run_media(
627 // Warm-up: wait until the capture ring actually has energy so we621 // Warm-up: wait until the capture ring actually has energy so we
628 // don't advertise "mic open" while still InputNotReady→silence.622 // don't advertise "mic open" while still InputNotReady→silence.
629 let peak = warm_up_mic(&mut *mic, std::time::Duration::from_millis(800));623 let peak = warm_up_mic(&mut *mic, std::time::Duration::from_millis(800));
630- log::info!(624+ log::info!("av-media: mic warm-up peak={peak:.4} (0 = capture silent / not ready)");
631- "av-media: mic warm-up peak={peak:.4} (0 = capture silent / not ready)"
632- );
633 if peak < 1e-5 {625 if peak < 1e-5 {
634 log::warn!(626 log::warn!(
635 "av-media: microphone opened but capture is silent so far — \627 "av-media: microphone opened but capture is silent so far — \
@@ -669,9 +661,7 @@ async fn run_media(
669 }661 }
670 }662 }
671 Err(e) => {663 Err(e) => {
672- log::warn!(664+ log::warn!("av-media: no microphone ({e}); publishing silence (listen-only)");
673- "av-media: no microphone ({e}); publishing silence (listen-only)"
674- );
675 muted.store(true, Ordering::Relaxed);665 muted.store(true, Ordering::Relaxed);
676 if let Err(e2) = broadcast.audio().set(666 if let Err(e2) = broadcast.audio().set(
677 MuteableSource::silence(muted.clone(), mic_level.clone()),667 MuteableSource::silence(muted.clone(), mic_level.clone()),
@@ -1201,19 +1191,14 @@ async fn tap_remote(
1201 // Match freeq-sdk-ffi: tighter latency than the 150ms streaming default.1191 // Match freeq-sdk-ffi: tighter latency than the 150ms streaming default.
1202 let policy = iroh_live::media::playout::PlaybackPolicy::default()1192 let policy = iroh_live::media::playout::PlaybackPolicy::default()
1203 .with_max_latency(std::time::Duration::from_millis(60));1193 .with_max_latency(std::time::Duration::from_millis(60));
1204- let remote = match RemoteBroadcast::with_playback_policy(1194+ let remote =
1205- &path,1195+ match RemoteBroadcast::with_playback_policy(&path, broadcast_consumer, policy).await {
1206- broadcast_consumer,1196+ Ok(r) => r,
1207- policy,1197+ Err(e) => {
1208- )1198+ log::warn!("av-media: catalog {path}: {e}");
1209- .await1199+ return;
1210- {1200+ }
1211- Ok(r) => r,1201+ };
1212- Err(e) => {
1213- log::warn!("av-media: catalog {path}: {e}");
1214- return;
1215- }
1216- };
1217 1202
1218 let audio_task = {1203 let audio_task = {
1219 let remote = remote.clone();1204 let remote = remote.clone();
@@ -1254,9 +1239,7 @@ async fn tap_remote(
1254 // the independent video pipeline (tap_remote waits on1239 // the independent video pipeline (tap_remote waits on
1255 // `remote.closed()`, not this task exiting).1240 // `remote.closed()`, not this task exiting).
1256 if consecutive_errs <= 3 || consecutive_errs % 10 == 0 {1241 if consecutive_errs <= 3 || consecutive_errs % 10 == 0 {
1257- log::warn!(1242+ log::warn!("av-media: audio sub {ps}: {e} (retry {consecutive_errs})");
1258- "av-media: audio sub {ps}: {e} (retry {consecutive_errs})"
1259- );
1260 }1243 }
1261 let backoff_ms = (500u64 * u64::from(consecutive_errs.min(8))).min(4_000);1244 let backoff_ms = (500u64 * u64::from(consecutive_errs.min(8))).min(4_000);
1262 tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;1245 tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
@@ -1395,10 +1378,7 @@ impl iroh_live::media::traits::AudioSource for SilenceSource {
1395 self.format1378 self.format
1396 }1379 }
1397 1380
1398- fn pop_samples(1381+ fn pop_samples(&mut self, buf: &mut [f32]) -> anyhow::Result<Option<usize>> {
1399- &mut self,
1400- buf: &mut [f32],
1401- ) -> anyhow::Result<Option<usize>> {
1402 for s in buf.iter_mut() {1382 for s in buf.iter_mut() {
1403 *s = 0.0;1383 *s = 0.0;
1404 }1384 }
@@ -1585,11 +1565,7 @@ fn open_camera_with_fallback(
1585 1565
1586 // Then non-virtual hardware (skip virtuals for auto-pick).1566 // Then non-virtual hardware (skip virtuals for auto-pick).
1587 let mut cams = listed.clone();1567 let mut cams = listed.clone();
1588- cams.sort_by(|a, b| {1568+ cams.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
1589- a.name
1590- .to_lowercase()
1591- .cmp(&b.name.to_lowercase())
1592- });
1593 for c in cams {1569 for c in cams {
1594 if is_virtual_camera(&c.name, &c.id) {1570 if is_virtual_camera(&c.name, &c.id) {
1595 continue;1571 continue;
@@ -1597,9 +1573,10 @@ fn open_camera_with_fallback(
1597 if c.supported_formats.is_empty() {1573 if c.supported_formats.is_empty() {
1598 continue;1574 continue;
1599 }1575 }
1600- if candidates.iter().any(|x| {1576+ if candidates
1601- x.as_deref() == Some(c.id.as_str()) || x.as_deref() == Some(c.name.as_str())1577+ .iter()
1602- }) {1578+ .any(|x| x.as_deref() == Some(c.id.as_str()) || x.as_deref() == Some(c.name.as_str()))
1579+ {
1603 continue;1580 continue;
1604 }1581 }
1605 candidates.push(Some(c.id));1582 candidates.push(Some(c.id));
@@ -1618,9 +1595,7 @@ fn open_camera_with_fallback(
1618 // prefer virtual.1595 // prefer virtual.
1619 let user_wants_virtual = preferred.is_some_and(|p| is_virt(p));1596 let user_wants_virtual = preferred.is_some_and(|p| is_virt(p));
1620 if is_virtual_camera(&name, label) && !user_wants_virtual {1597 if is_virtual_camera(&name, label) && !user_wants_virtual {
1621- log::warn!(1598+ log::warn!("av-media: rejecting auto-opened virtual camera {name} ({label})");
1622- "av-media: rejecting auto-opened virtual camera {name} ({label})"
1623- );
1624 errors.push(format!("{label}: rejected virtual {name}"));1599 errors.push(format!("{label}: rejected virtual {name}"));
1625 continue;1600 continue;
1626 }1601 }
@@ -1702,7 +1677,9 @@ fn open_camera_with_busy_retry(
1702 let busy = e.to_string().to_ascii_lowercase().contains("busy");1677 let busy = e.to_string().to_ascii_lowercase().contains("busy");
1703 last = Some(e);1678 last = Some(e);
1704 if busy && attempt + 1 < ATTEMPTS {1679 if busy && attempt + 1 < ATTEMPTS {
1705- std::thread::sleep(std::time::Duration::from_millis(150 * (attempt + 1) as u64));1680+ std::thread::sleep(std::time::Duration::from_millis(
1681+ 150 * (attempt + 1) as u64,
1682+ ));
1706 continue;1683 continue;
1707 }1684 }
1708 break;1685 break;
@@ -1784,9 +1761,7 @@ fn spawn_local_preview_pump(
1784 }1761 }
1785 }1762 }
1786 Err(_) => {1763 Err(_) => {
1787- log::warn!(1764+ log::warn!("av-media: local preview pump rgba_image panicked {w}x{h}");
1788- "av-media: local preview pump rgba_image panicked {w}x{h}"
1789- );
1790 }1765 }
1791 }1766 }
1792 } else if track.is_closed() {1767 } else if track.is_closed() {
@@ -1831,8 +1806,7 @@ fn open_android_camera(
1831 local_frame_count: Arc<AtomicU64>,1806 local_frame_count: Arc<AtomicU64>,
1832 want_publish: bool,1807 want_publish: bool,
1833 ) -> Result<crate::android_camera::CameraCaptureGuard> {1808 ) -> Result<crate::android_camera::CameraCaptureGuard> {
1834- crate::android_camera::start_capture(camera_id)1809+ crate::android_camera::start_capture(camera_id).context("CameraCapture.start")?;
1835- .context("CameraCapture.start")?;
1836 // Camera2 open + session configure is async on a Java handler thread.1810 // Camera2 open + session configure is async on a Java handler thread.
1837 if let Err(e) = crate::android_camera::wait_until_opened(std::time::Duration::from_secs(5)) {1811 if let Err(e) = crate::android_camera::wait_until_opened(std::time::Duration::from_secs(5)) {
1838 crate::android_camera::stop_capture();1812 crate::android_camera::stop_capture();
@@ -1917,9 +1891,7 @@ impl VideoSource for GatedCameraSource {
1917 self.inner.stop()1891 self.inner.stop()
1918 }1892 }
1919 1893
1920- fn pop_frame(1894+ fn pop_frame(&mut self) -> anyhow::Result<Option<iroh_live::media::format::VideoFrame>> {
1921- &mut self,
1922- ) -> anyhow::Result<Option<iroh_live::media::format::VideoFrame>> {
1923 // Check publish flag *before* capturing. The previous order (dqbuf then1895 // Check publish flag *before* capturing. The previous order (dqbuf then
1924 // discard) left V4L2 STREAMON / the privacy LED on for the whole mute.1896 // discard) left V4L2 STREAMON / the privacy LED on for the whole mute.
1925 if !self.enabled.load(Ordering::Relaxed) {1897 if !self.enabled.load(Ordering::Relaxed) {
@@ -2075,10 +2047,7 @@ impl iroh_live::media::traits::AudioSource for ToneSource {
2075 self.format2047 self.format
2076 }2048 }
2077 2049
2078- fn pop_samples(2050+ fn pop_samples(&mut self, buf: &mut [f32]) -> anyhow::Result<Option<usize>> {
2079- &mut self,
2080- buf: &mut [f32],
2081- ) -> anyhow::Result<Option<usize>> {
2082 let channels = self.format.channel_count.max(1) as usize;2051 let channels = self.format.channel_count.max(1) as usize;
2083 let frames = buf.len() / channels;2052 let frames = buf.len() / channels;
2084 let phase_inc = self.frequency / self.format.sample_rate as f32;2053 let phase_inc = self.frequency / self.format.sample_rate as f32;
@@ -2170,10 +2139,7 @@ impl iroh_live::media::traits::AudioSource for MuteableSource {
2170 self.inner.format()2139 self.inner.format()
2171 }2140 }
2172 2141
2173- fn pop_samples(2142+ fn pop_samples(&mut self, buf: &mut [f32]) -> anyhow::Result<Option<usize>> {
2174- &mut self,
2175- buf: &mut [f32],
2176- ) -> anyhow::Result<Option<usize>> {
2177 let mut pre_peak = 0.0f32;2143 let mut pre_peak = 0.0f32;
2178 let n = match self.inner.pop_samples(buf)? {2144 let n = match self.inner.pop_samples(buf)? {
2179 Some(n) if n > 0 => {2145 Some(n) if n > 0 => {
@@ -2238,9 +2204,7 @@ mod tests {
2238 use iroh_live::media::playout::PlaybackPolicy;2204 use iroh_live::media::playout::PlaybackPolicy;
2239 use iroh_live::media::publish::LocalBroadcast;2205 use iroh_live::media::publish::LocalBroadcast;
2240 use iroh_live::media::subscribe::RemoteBroadcast;2206 use iroh_live::media::subscribe::RemoteBroadcast;
2241- use iroh_live::media::traits::{2207+ use iroh_live::media::traits::{AudioSink, AudioSinkHandle, AudioSource, AudioStreamFactory};
2242- AudioSink, AudioSinkHandle, AudioSource, AudioStreamFactory,
2243- };
2244 use n0_future::boxed::BoxFuture;2208 use n0_future::boxed::BoxFuture;
2245 use std::time::Duration;2209 use std::time::Duration;
2246 2210
@@ -2278,9 +2242,7 @@ mod tests {
2278 }2242 }
2279 let _ = cam.stop();2243 let _ = cam.stop();
2280 let Some(f) = frame else {2244 let Some(f) = frame else {
2281- eprintln!(2245+ eprintln!("SKIP real_camera id={id:?}: no frames (OBS Virtual Camera not started?)");
2282- "SKIP real_camera id={id:?}: no frames (OBS Virtual Camera not started?)"
2283- );
2284 return;2246 return;
2285 };2247 };
2286 let (w, h) = (f.width(), f.height());2248 let (w, h) = (f.width(), f.height());
@@ -2291,9 +2253,7 @@ mod tests {
2291 .get(0..4)2253 .get(0..4)
2292 .map(|p| (p[0], p[1], p[2], p[3]))2254 .map(|p| (p[0], p[1], p[2], p[3]))
2293 .unwrap_or((0, 0, 0, 0));2255 .unwrap_or((0, 0, 0, 0));
2294- eprintln!(2256+ eprintln!("camera id={id:?} {w}x{h} rgba0=({r0},{g0},{b0},{a0}) luma range {min}..{max}");
2295- "camera id={id:?} {w}x{h} rgba0=({r0},{g0},{b0},{a0}) luma range {min}..{max}"
2296- );
2297 assert!(2257 assert!(
2298 max > min,2258 max > min,
2299 "real camera frame must be non-uniform (luma range {min}..{max}); \2259 "real camera frame must be non-uniform (luma range {min}..{max}); \
@@ -2394,10 +2354,7 @@ mod tests {
2394 assert_eq!(n, Some(buf.len()));2354 assert_eq!(n, Some(buf.len()));
2395 assert_eq!(pcm_rms(&buf), 0.0, "muted must be silence");2355 assert_eq!(pcm_rms(&buf), 0.0, "muted must be silence");
2396 // Level still saw pre-mute energy from the tone.2356 // Level still saw pre-mute energy from the tone.
2397- assert!(2357+ assert!(src.level.get() > 0.01, "mic meter should move while muted");
2398- src.level.get() > 0.01,
2399- "mic meter should move while muted"
2400- );
2401 }2358 }
2402 2359
2403 #[test]2360 #[test]
@@ -2561,13 +2518,10 @@ mod tests {
2561 // Keep producer alive for the duration of the test.2518 // Keep producer alive for the duration of the test.
2562 let _keepalive = broadcast;2519 let _keepalive = broadcast;
2563 2520
2564- let remote = RemoteBroadcast::with_playback_policy(2521+ let remote =
2565- &path,2522+ RemoteBroadcast::with_playback_policy(&path, consumer, PlaybackPolicy::unmanaged())
2566- consumer,2523+ .await
2567- PlaybackPolicy::unmanaged(),2524+ .expect("catalog from LocalBroadcast");
2568- )
2569- .await
2570- .expect("catalog from LocalBroadcast");
2571 2525
2572 let (tx, rx) = std::sync::mpsc::sync_channel::<Vec<f32>>(64);2526 let (tx, rx) = std::sync::mpsc::sync_channel::<Vec<f32>>(64);
2573 let backend = TapBackend { tx };2527 let backend = TapBackend { tx };
@@ -2796,12 +2750,8 @@ mod tests {
2796 "video.clear must drop the source so privacy LED can go dark"2750 "video.clear must drop the source so privacy LED can go dark"
2797 );2751 );
2798 assert!(2752 assert!(
2799- !store2753+ !store.snapshot().iter().any(|(k, _)| k == LOCAL_PREVIEW_KEY),
2800- .snapshot()
2801- .iter()
2802- .any(|(k, _)| k == LOCAL_PREVIEW_KEY),
2803 "local preview frame must be cleared on release"2754 "local preview frame must be cleared on release"
2804 );2755 );
2805 }2756 }
2806-
2807 }2757 }
modified crates/jolt-moq/src/lib.rs +11 -7
@@ -43,12 +43,12 @@ pub mod av_media;
4343 mod nv12_orient;
4444 // V4L2 is Linux's camera interface and the phone does not offer it; there the
4545 // camera is Java, reached through the two modules below.
46-#[cfg(not(target_os = "android"))]
47-mod v4l2cam;
4846 #[cfg(target_os = "android")]
4947 mod android_camera;
5048 #[cfg(target_os = "android")]
5149 mod android_jni;
50+#[cfg(not(target_os = "android"))]
51+mod v4l2cam;
5252
5353 use std::collections::HashMap;
5454 use std::ffi::{c_char, c_int};
@@ -768,7 +768,9 @@ mod tests {
768768 use std::ffi::{CStr, CString};
769769
770770 fn read(ptr: *const c_char) -> String {
771- unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned()
771+ unsafe { CStr::from_ptr(ptr) }
772+ .to_string_lossy()
773+ .into_owned()
772774 }
773775
774776 /// There is one session in this library, so there is one in its tests, and
@@ -831,9 +833,8 @@ mod tests {
831833 #[test]
832834 fn a_server_that_is_not_a_url_answers_the_empty_string() {
833835 let server = CString::new(" ").unwrap();
834- let url = read(unsafe {
835- joltmoq_sfu_url(server.as_ptr(), std::ptr::null(), std::ptr::null())
836- });
836+ let url =
837+ read(unsafe { joltmoq_sfu_url(server.as_ptr(), std::ptr::null(), std::ptr::null()) });
837838 assert_eq!(url, "");
838839 }
839840
@@ -846,7 +847,10 @@ mod tests {
846847 unsafe { joltmoq_can_dial(remote.as_ptr(), std::ptr::null()) },
847848 0
848849 );
849- assert_eq!(unsafe { joltmoq_can_dial(remote.as_ptr(), jwt.as_ptr()) }, 1);
850+ assert_eq!(
851+ unsafe { joltmoq_can_dial(remote.as_ptr(), jwt.as_ptr()) },
852+ 1
853+ );
850854 assert_eq!(
851855 unsafe { joltmoq_can_dial(local.as_ptr(), std::ptr::null()) },
852856 1
@@ -43,12 +43,12 @@ pub mod av_media;
43 mod nv12_orient;43 mod nv12_orient;
44 // V4L2 is Linux's camera interface and the phone does not offer it; there the44 // V4L2 is Linux's camera interface and the phone does not offer it; there the
45 // camera is Java, reached through the two modules below.45 // camera is Java, reached through the two modules below.
46-#[cfg(not(target_os = "android"))]
47-mod v4l2cam;
48 #[cfg(target_os = "android")]46 #[cfg(target_os = "android")]
49 mod android_camera;47 mod android_camera;
50 #[cfg(target_os = "android")]48 #[cfg(target_os = "android")]
51 mod android_jni;49 mod android_jni;
50+#[cfg(not(target_os = "android"))]
51+mod v4l2cam;
52 52
53 use std::collections::HashMap;53 use std::collections::HashMap;
54 use std::ffi::{c_char, c_int};54 use std::ffi::{c_char, c_int};
@@ -768,7 +768,9 @@ mod tests {
768 use std::ffi::{CStr, CString};768 use std::ffi::{CStr, CString};
769 769
770 fn read(ptr: *const c_char) -> String {770 fn read(ptr: *const c_char) -> String {
771- unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned()771+ unsafe { CStr::from_ptr(ptr) }
772+ .to_string_lossy()
773+ .into_owned()
772 }774 }
773 775
774 /// There is one session in this library, so there is one in its tests, and776 /// There is one session in this library, so there is one in its tests, and
@@ -831,9 +833,8 @@ mod tests {
831 #[test]833 #[test]
832 fn a_server_that_is_not_a_url_answers_the_empty_string() {834 fn a_server_that_is_not_a_url_answers_the_empty_string() {
833 let server = CString::new(" ").unwrap();835 let server = CString::new(" ").unwrap();
834- let url = read(unsafe {836+ let url =
835- joltmoq_sfu_url(server.as_ptr(), std::ptr::null(), std::ptr::null())837+ read(unsafe { joltmoq_sfu_url(server.as_ptr(), std::ptr::null(), std::ptr::null()) });
836- });
837 assert_eq!(url, "");838 assert_eq!(url, "");
838 }839 }
839 840
@@ -846,7 +847,10 @@ mod tests {
846 unsafe { joltmoq_can_dial(remote.as_ptr(), std::ptr::null()) },847 unsafe { joltmoq_can_dial(remote.as_ptr(), std::ptr::null()) },
847 0848 0
848 );849 );
849- assert_eq!(unsafe { joltmoq_can_dial(remote.as_ptr(), jwt.as_ptr()) }, 1);850+ assert_eq!(
851+ unsafe { joltmoq_can_dial(remote.as_ptr(), jwt.as_ptr()) },
852+ 1
853+ );
850 assert_eq!(854 assert_eq!(
851 unsafe { joltmoq_can_dial(local.as_ptr(), std::ptr::null()) },855 unsafe { joltmoq_can_dial(local.as_ptr(), std::ptr::null()) },
852 1856 1
modified crates/jolt-moq/src/v4l2cam.rs +3 -6
@@ -96,11 +96,7 @@ impl V4l2MmapCapture {
9696 // Try requested size with YUYV first (loopback native), then MJPG,
9797 // then let the driver pick (0x0 keeps current).
9898 let mut actual: Option<Format> = None;
99- for (req_w, req_h, fourcc) in [
100- (w, h, *b"YUYV"),
101- (w, h, *b"MJPG"),
102- (0, 0, *b"YUYV"),
103- ] {
99+ for (req_w, req_h, fourcc) in [(w, h, *b"YUYV"), (w, h, *b"MJPG"), (0, 0, *b"YUYV")] {
104100 let desired = Format {
105101 width: req_w,
106102 height: req_h,
@@ -421,7 +417,8 @@ mod tests {
421417 let mut min = 255u8;
422418 let mut max = 0u8;
423419 for px in bytes.chunks_exact(4) {
424- let l = ((77u32 * px[0] as u32 + 150u32 * px[1] as u32 + 29u32 * px[2] as u32) >> 8) as u8;
420+ let l =
421+ ((77u32 * px[0] as u32 + 150u32 * px[1] as u32 + 29u32 * px[2] as u32) >> 8) as u8;
425422 min = min.min(l);
426423 max = max.max(l);
427424 }
@@ -96,11 +96,7 @@ impl V4l2MmapCapture {
96 // Try requested size with YUYV first (loopback native), then MJPG,96 // Try requested size with YUYV first (loopback native), then MJPG,
97 // then let the driver pick (0x0 keeps current).97 // then let the driver pick (0x0 keeps current).
98 let mut actual: Option<Format> = None;98 let mut actual: Option<Format> = None;
99- for (req_w, req_h, fourcc) in [99+ for (req_w, req_h, fourcc) in [(w, h, *b"YUYV"), (w, h, *b"MJPG"), (0, 0, *b"YUYV")] {
100- (w, h, *b"YUYV"),
101- (w, h, *b"MJPG"),
102- (0, 0, *b"YUYV"),
103- ] {
104 let desired = Format {100 let desired = Format {
105 width: req_w,101 width: req_w,
106 height: req_h,102 height: req_h,
@@ -421,7 +417,8 @@ mod tests {
421 let mut min = 255u8;417 let mut min = 255u8;
422 let mut max = 0u8;418 let mut max = 0u8;
423 for px in bytes.chunks_exact(4) {419 for px in bytes.chunks_exact(4) {
424- let l = ((77u32 * px[0] as u32 + 150u32 * px[1] as u32 + 29u32 * px[2] as u32) >> 8) as u8;420+ let l =
421+ ((77u32 * px[0] as u32 + 150u32 * px[1] as u32 + 29u32 * px[2] as u32) >> 8) as u8;
425 min = min.min(l);422 min = min.min(l);
426 max = max.max(l);423 max = max.max(l);
427 }424 }
modified crates/jolt-tui/src/graphics.rs +18 -8
@@ -50,9 +50,8 @@ pub fn png_size(bytes: &[u8]) -> Option<(u32, u32)> {
5050 if bytes.len() < 24 || bytes[..8] != SIGNATURE || &bytes[12..16] != b"IHDR" {
5151 return None;
5252 }
53- let read = |at: usize| {
54- u32::from_be_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
55- };
53+ let read =
54+ |at: usize| u32::from_be_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]);
5655 let (w, h) = (read(16), read(20));
5756 (w > 0 && h > 0).then_some((w, h))
5857 }
@@ -206,7 +205,12 @@ impl Graphics {
206205 if still {
207206 self.placed.insert(node, was);
208207 } else {
209- write!(out, "\x1b_Ga=d,d=i,i={},p={},q=2\x1b\\", self.id(&was.path), node)?;
208+ write!(
209+ out,
210+ "\x1b_Ga=d,d=i,i={},p={},q=2\x1b\\",
211+ self.id(&was.path),
212+ node
213+ )?;
210214 }
211215 }
212216 for placement in now {
@@ -300,11 +304,14 @@ impl Graphics {
300304
301305 /// Standard base64, which is what the protocol's payload is written in.
302306 fn base64(bytes: &[u8]) -> String {
303- const ALPHABET: &[u8; 64] =
304- b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
307+ const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
305308 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
306309 for group in bytes.chunks(3) {
307- let b = [group[0], *group.get(1).unwrap_or(&0), *group.get(2).unwrap_or(&0)];
310+ let b = [
311+ group[0],
312+ *group.get(1).unwrap_or(&0),
313+ *group.get(2).unwrap_or(&0),
314+ ];
308315 let n = u32::from_be_bytes([0, b[0], b[1], b[2]]);
309316 let mut quad = [0u8; 4];
310317 for (i, slot) in quad.iter_mut().enumerate() {
@@ -330,7 +337,10 @@ mod tests {
330337 assert_eq!(base64(b"fo"), "Zm8=");
331338 assert_eq!(base64(b"foo"), "Zm9v");
332339 assert_eq!(base64(b"foob"), "Zm9vYg==");
333- assert_eq!(base64(b"any carnal pleasure."), "YW55IGNhcm5hbCBwbGVhc3VyZS4=");
340+ assert_eq!(
341+ base64(b"any carnal pleasure."),
342+ "YW55IGNhcm5hbCBwbGVhc3VyZS4="
343+ );
334344 }
335345
336346 #[test]
@@ -50,9 +50,8 @@ pub fn png_size(bytes: &[u8]) -> Option<(u32, u32)> {
50 if bytes.len() < 24 || bytes[..8] != SIGNATURE || &bytes[12..16] != b"IHDR" {50 if bytes.len() < 24 || bytes[..8] != SIGNATURE || &bytes[12..16] != b"IHDR" {
51 return None;51 return None;
52 }52 }
53- let read = |at: usize| {53+ let read =
54- u32::from_be_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])54+ |at: usize| u32::from_be_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]);
55- };
56 let (w, h) = (read(16), read(20));55 let (w, h) = (read(16), read(20));
57 (w > 0 && h > 0).then_some((w, h))56 (w > 0 && h > 0).then_some((w, h))
58 }57 }
@@ -206,7 +205,12 @@ impl Graphics {
206 if still {205 if still {
207 self.placed.insert(node, was);206 self.placed.insert(node, was);
208 } else {207 } else {
209- write!(out, "\x1b_Ga=d,d=i,i={},p={},q=2\x1b\\", self.id(&was.path), node)?;208+ write!(
209+ out,
210+ "\x1b_Ga=d,d=i,i={},p={},q=2\x1b\\",
211+ self.id(&was.path),
212+ node
213+ )?;
210 }214 }
211 }215 }
212 for placement in now {216 for placement in now {
@@ -300,11 +304,14 @@ impl Graphics {
300 304
301 /// Standard base64, which is what the protocol's payload is written in.305 /// Standard base64, which is what the protocol's payload is written in.
302 fn base64(bytes: &[u8]) -> String {306 fn base64(bytes: &[u8]) -> String {
303- const ALPHABET: &[u8; 64] =307+ const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
304- b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
305 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);308 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
306 for group in bytes.chunks(3) {309 for group in bytes.chunks(3) {
307- let b = [group[0], *group.get(1).unwrap_or(&0), *group.get(2).unwrap_or(&0)];310+ let b = [
311+ group[0],
312+ *group.get(1).unwrap_or(&0),
313+ *group.get(2).unwrap_or(&0),
314+ ];
308 let n = u32::from_be_bytes([0, b[0], b[1], b[2]]);315 let n = u32::from_be_bytes([0, b[0], b[1], b[2]]);
309 let mut quad = [0u8; 4];316 let mut quad = [0u8; 4];
310 for (i, slot) in quad.iter_mut().enumerate() {317 for (i, slot) in quad.iter_mut().enumerate() {
@@ -330,7 +337,10 @@ mod tests {
330 assert_eq!(base64(b"fo"), "Zm8=");337 assert_eq!(base64(b"fo"), "Zm8=");
331 assert_eq!(base64(b"foo"), "Zm9v");338 assert_eq!(base64(b"foo"), "Zm9v");
332 assert_eq!(base64(b"foob"), "Zm9vYg==");339 assert_eq!(base64(b"foob"), "Zm9vYg==");
333- assert_eq!(base64(b"any carnal pleasure."), "YW55IGNhcm5hbCBwbGVhc3VyZS4=");340+ assert_eq!(
341+ base64(b"any carnal pleasure."),
342+ "YW55IGNhcm5hbCBwbGVhc3VyZS4="
343+ );
334 }344 }
335 345
336 #[test]346 #[test]
modified crates/jolt-tui/src/paint.rs +9 -7
@@ -9,8 +9,8 @@
99 //! over the whole screen, so it is painted after everything else at the size it
1010 //! asked for, in the middle.
1111
12-use crate::layout::{self, wrap, Align};
1312 use crate::graphics;
13+use crate::layout::{self, wrap, Align};
1414 use crate::screen::{self, attr, Color, Rect, Screen, Style};
1515 use crate::tree::{Props, Tag, Tree};
1616
@@ -283,8 +283,13 @@ impl Painter<'_> {
283283 return;
284284 }
285285 if !graphics::supported() {
286- self.screen
287- .text(area.x, area.y, area.w, layout::PICTURE, style.with(attr::DIM));
286+ self.screen.text(
287+ area.x,
288+ area.y,
289+ area.w,
290+ layout::PICTURE,
291+ style.with(attr::DIM),
292+ );
288293 return;
289294 }
290295 // The column hands a child its whole width; a picture takes only what
@@ -384,10 +389,7 @@ impl Painter<'_> {
384389 let col = if showing_placeholder {
385390 0
386391 } else {
387- let typed: String = visible
388- .chars()
389- .take(caret.saturating_sub(from))
390- .collect();
392+ let typed: String = visible.chars().take(caret.saturating_sub(from)).collect();
391393 (screen::text_cols(&typed)).min(area.w.saturating_sub(1))
392394 };
393395 self.out.cursor = Some((area.x.saturating_add(col), area.y + i as u16));
@@ -9,8 +9,8 @@
9 //! over the whole screen, so it is painted after everything else at the size it9 //! over the whole screen, so it is painted after everything else at the size it
10 //! asked for, in the middle.10 //! asked for, in the middle.
11 11
12-use crate::layout::{self, wrap, Align};
13 use crate::graphics;12 use crate::graphics;
13+use crate::layout::{self, wrap, Align};
14 use crate::screen::{self, attr, Color, Rect, Screen, Style};14 use crate::screen::{self, attr, Color, Rect, Screen, Style};
15 use crate::tree::{Props, Tag, Tree};15 use crate::tree::{Props, Tag, Tree};
16 16
@@ -283,8 +283,13 @@ impl Painter<'_> {
283 return;283 return;
284 }284 }
285 if !graphics::supported() {285 if !graphics::supported() {
286- self.screen286+ self.screen.text(
287- .text(area.x, area.y, area.w, layout::PICTURE, style.with(attr::DIM));287+ area.x,
288+ area.y,
289+ area.w,
290+ layout::PICTURE,
291+ style.with(attr::DIM),
292+ );
288 return;293 return;
289 }294 }
290 // The column hands a child its whole width; a picture takes only what295 // The column hands a child its whole width; a picture takes only what
@@ -384,10 +389,7 @@ impl Painter<'_> {
384 let col = if showing_placeholder {389 let col = if showing_placeholder {
385 0390 0
386 } else {391 } else {
387- let typed: String = visible392+ let typed: String = visible.chars().take(caret.saturating_sub(from)).collect();
388- .chars()
389- .take(caret.saturating_sub(from))
390- .collect();
391 (screen::text_cols(&typed)).min(area.w.saturating_sub(1))393 (screen::text_cols(&typed)).min(area.w.saturating_sub(1))
392 };394 };
393 self.out.cursor = Some((area.x.saturating_add(col), area.y + i as u16));395 self.out.cursor = Some((area.x.saturating_add(col), area.y + i as u16));
modified crates/jolt-tui/src/term.rs +1 -1
@@ -16,8 +16,8 @@ use crossterm::terminal::{
1616 };
1717 use crossterm::{cursor, execute, queue, style};
1818
19-use crate::keys;
2019 use crate::graphics::{self, Graphics, Placement};
20+use crate::keys;
2121 use crate::screen::{self, attr, Color, Screen, Style};
2222
2323 /// How far one notch of the wheel moves a list, in rows.
@@ -16,8 +16,8 @@ use crossterm::terminal::{
16 };16 };
17 use crossterm::{cursor, execute, queue, style};17 use crossterm::{cursor, execute, queue, style};
18 18
19-use crate::keys;
20 use crate::graphics::{self, Graphics, Placement};19 use crate::graphics::{self, Graphics, Placement};
20+use crate::keys;
21 use crate::screen::{self, attr, Color, Screen, Style};21 use crate::screen::{self, attr, Color, Screen, Style};
22 22
23 /// How far one notch of the wheel moves a list, in rows.23 /// How far one notch of the wheel moves a list, in rows.
modified crates/jolt-tui/src/tests.rs +14 -4
@@ -316,12 +316,22 @@ fn the_wheel_scrolls_the_list_under_the_pointer_not_the_first_one_painted() {
316316 let sidebar = node(&mut ui, row, "scroll", &[]);
317317 ui.tree.set(sidebar, "width-request", Value::Num(10.0));
318318 for i in 0..6 {
319- node(&mut ui, sidebar, "label", &[("label", &format!("chat {i}"))]);
319+ node(
320+ &mut ui,
321+ sidebar,
322+ "label",
323+ &[("label", &format!("chat {i}"))],
324+ );
320325 }
321326 let backlog = node(&mut ui, row, "scroll", &[]);
322327 ui.tree.set(backlog, "width-request", Value::Num(10.0));
323328 for i in 0..6 {
324- node(&mut ui, backlog, "label", &[("label", &format!("line {i}"))]);
329+ node(
330+ &mut ui,
331+ backlog,
332+ "label",
333+ &[("label", &format!("line {i}"))],
334+ );
325335 }
326336 ui.frame();
327337 assert_eq!(ui.screen.line(0), "chat 0 line 0");
@@ -680,9 +690,9 @@ fn a_sticky_viewport_opens_at_the_bottom_and_stays_there() {
680690 ui.frame();
681691 assert_eq!(ui.screen.line(0), "line 1");
682692 ui.tree.clear_props(scroll);
683- ui.tree.set(scroll, "scroll-key", Value::Str("backlog".into()));
684693 ui.tree
685- .set(scroll, "stick-to-bottom", Value::Bool(true));
694+ .set(scroll, "scroll-key", Value::Str("backlog".into()));
695+ ui.tree.set(scroll, "stick-to-bottom", Value::Bool(true));
686696 ui.frame();
687697 assert_eq!(ui.screen.line(0), "line 1");
688698
@@ -316,12 +316,22 @@ fn the_wheel_scrolls_the_list_under_the_pointer_not_the_first_one_painted() {
316 let sidebar = node(&mut ui, row, "scroll", &[]);316 let sidebar = node(&mut ui, row, "scroll", &[]);
317 ui.tree.set(sidebar, "width-request", Value::Num(10.0));317 ui.tree.set(sidebar, "width-request", Value::Num(10.0));
318 for i in 0..6 {318 for i in 0..6 {
319- node(&mut ui, sidebar, "label", &[("label", &format!("chat {i}"))]);319+ node(
320+ &mut ui,
321+ sidebar,
322+ "label",
323+ &[("label", &format!("chat {i}"))],
324+ );
320 }325 }
321 let backlog = node(&mut ui, row, "scroll", &[]);326 let backlog = node(&mut ui, row, "scroll", &[]);
322 ui.tree.set(backlog, "width-request", Value::Num(10.0));327 ui.tree.set(backlog, "width-request", Value::Num(10.0));
323 for i in 0..6 {328 for i in 0..6 {
324- node(&mut ui, backlog, "label", &[("label", &format!("line {i}"))]);329+ node(
330+ &mut ui,
331+ backlog,
332+ "label",
333+ &[("label", &format!("line {i}"))],
334+ );
325 }335 }
326 ui.frame();336 ui.frame();
327 assert_eq!(ui.screen.line(0), "chat 0 line 0");337 assert_eq!(ui.screen.line(0), "chat 0 line 0");
@@ -680,9 +690,9 @@ fn a_sticky_viewport_opens_at_the_bottom_and_stays_there() {
680 ui.frame();690 ui.frame();
681 assert_eq!(ui.screen.line(0), "line 1");691 assert_eq!(ui.screen.line(0), "line 1");
682 ui.tree.clear_props(scroll);692 ui.tree.clear_props(scroll);
683- ui.tree.set(scroll, "scroll-key", Value::Str("backlog".into()));
684 ui.tree693 ui.tree
685- .set(scroll, "stick-to-bottom", Value::Bool(true));694+ .set(scroll, "scroll-key", Value::Str("backlog".into()));
695+ ui.tree.set(scroll, "stick-to-bottom", Value::Bool(true));
686 ui.frame();696 ui.frame();
687 assert_eq!(ui.screen.line(0), "line 1");697 assert_eq!(ui.screen.line(0), "line 1");
688 698
modified crates/jolt-vidya/src/lib.rs +3 -1
@@ -273,7 +273,9 @@ pub unsafe extern "C" fn vidya_checkbox(label: *const c_char, checked: *mut c_in
273273 pub unsafe extern "C" fn vidya_checkbox_value(label: *const c_char, checked: c_int) -> c_int {
274274 let label = borrowed_str(label);
275275 let current = checked != 0;
276- with_ui(current, |ui, theme| ui::checkbox(ui, theme, current, &label).0) as c_int
276+ with_ui(current, |ui, theme| {
277+ ui::checkbox(ui, theme, current, &label).0
278+ }) as c_int
277279 }
278280
279281 /// # Safety
@@ -273,7 +273,9 @@ pub unsafe extern "C" fn vidya_checkbox(label: *const c_char, checked: *mut c_in
273 pub unsafe extern "C" fn vidya_checkbox_value(label: *const c_char, checked: c_int) -> c_int {273 pub unsafe extern "C" fn vidya_checkbox_value(label: *const c_char, checked: c_int) -> c_int {
274 let label = borrowed_str(label);274 let label = borrowed_str(label);
275 let current = checked != 0;275 let current = checked != 0;
276- with_ui(current, |ui, theme| ui::checkbox(ui, theme, current, &label).0) as c_int276+ with_ui(current, |ui, theme| {
277+ ui::checkbox(ui, theme, current, &label).0
278+ }) as c_int
277 }279 }
278 280
279 /// # Safety281 /// # Safety
modified crates/jolt-vidya/src/tree.rs +31 -23
@@ -322,10 +322,8 @@ impl Tree {
322322 if rgba.len() != expected {
323323 return false;
324324 }
325- let image = egui::ColorImage::from_rgba_unmultiplied(
326- [width as usize, height as usize],
327- rgba,
328- );
325+ let image =
326+ egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], rgba);
329327 // Overwrites whatever had not been painted yet: the newest frame is
330328 // the only one worth showing, and a backlog of stale ones is latency.
331329 self.feeds.entry(key.to_owned()).or_default().pending = Some(image);
@@ -1069,7 +1067,6 @@ impl Tree {
10691067 // whatever it is given, and MAX minus anything is still
10701068 // MAX — an offset the content can never reach, which left
10711069 // the area painting nothing at all.
1072-
10731070 });
10741071
10751072 // Say when the view leaves the end and when it comes back, so
@@ -1091,7 +1088,11 @@ impl Tree {
10911088 let end_key = key.with("at_end");
10921089 let away_key = key.with("away_frames");
10931090 let away_frames = ui.ctx().data(|d| d.get_temp::<u32>(away_key)).unwrap_or(0);
1094- let away_frames = if at_end { 0 } else { away_frames.saturating_add(1) };
1091+ let away_frames = if at_end {
1092+ 0
1093+ } else {
1094+ away_frames.saturating_add(1)
1095+ };
10951096 ui.ctx().data_mut(|d| d.insert_temp(away_key, away_frames));
10961097
10971098 let settled = if at_end {
@@ -1223,10 +1224,7 @@ impl Tree {
12231224 ..
12241225 } if modifiers.command
12251226 )
1226- }) && !i
1227- .events
1228- .iter()
1229- .any(|e| matches!(e, egui::Event::Paste(_)))
1227+ }) && !i.events.iter().any(|e| matches!(e, egui::Event::Paste(_)))
12301228 });
12311229 if paste_without_text {
12321230 self.emit(id, "paste-empty", String::new(), 0.0);
@@ -1283,8 +1281,11 @@ impl Tree {
12831281 .next()
12841282 .map(|c| c.to_uppercase().to_string())
12851283 .unwrap_or_else(|| "?".to_owned());
1286- ui.painter()
1287- .circle_filled(rect.center(), size * 0.5, name_colour(&label, theme));
1284+ ui.painter().circle_filled(
1285+ rect.center(),
1286+ size * 0.5,
1287+ name_colour(&label, theme),
1288+ );
12881289 ui.painter().text(
12891290 rect.center(),
12901291 Align2::CENTER_CENTER,
@@ -1416,10 +1417,8 @@ impl Tree {
14161417 return;
14171418 }
14181419 let scale = (space.x / size.x).min(space.y / size.y);
1419- let (rect, response) =
1420- ui.allocate_exact_size(space, egui::Sense::click());
1421- let painted =
1422- egui::Rect::from_center_size(rect.center(), size * scale);
1420+ let (rect, response) = ui.allocate_exact_size(space, egui::Sense::click());
1421+ let painted = egui::Rect::from_center_size(rect.center(), size * scale);
14231422 egui::Image::new(egui::load::SizedTexture::new(texture.id(), size * scale))
14241423 .paint_at(ui, painted);
14251424 if response.clicked() {
@@ -1483,9 +1482,7 @@ impl Tree {
14831482 // otherwise for one of them. A row that sits at the bottom of a screen
14841483 // wants its space above it, not under it, and that is not a thing a
14851484 // single number can express.
1486- let side = |key: &str| {
1487- props.num(key, props.num("margin", 0.0)).clamp(0.0, 127.0) as i8
1488- };
1485+ let side = |key: &str| props.num(key, props.num("margin", 0.0)).clamp(0.0, 127.0) as i8;
14891486 let margin = Margin {
14901487 left: side("margin-left"),
14911488 right: side("margin-right"),
@@ -1571,7 +1568,9 @@ mod tests {
15711568 let ctx = egui::Context::default();
15721569
15731570 let mut input = egui::RawInput::default();
1574- input.events.push(egui::Event::PointerMoved(egui::pos2(20.0, 20.0)));
1571+ input
1572+ .events
1573+ .push(egui::Event::PointerMoved(egui::pos2(20.0, 20.0)));
15751574 let _ = ctx.run(input.clone(), |ctx| {
15761575 egui::CentralPanel::default().show(ctx, |ui| tree.paint(ui, &theme));
15771576 });
@@ -1591,7 +1590,9 @@ mod tests {
15911590 let card = tree.new_node("window");
15921591 tree.append(pill, card);
15931592 let mut input = egui::RawInput::default();
1594- input.events.push(egui::Event::PointerMoved(egui::pos2(20.0, 20.0)));
1593+ input
1594+ .events
1595+ .push(egui::Event::PointerMoved(egui::pos2(20.0, 20.0)));
15951596 let _ = ctx.run(input, |ctx| {
15961597 egui::CentralPanel::default().show(ctx, |ui| tree.paint(ui, &theme));
15971598 });
@@ -1741,7 +1742,10 @@ mod tests {
17411742 let mut tree = Tree::default();
17421743 let id = tree.new_node("carousel");
17431744 assert!(tree.exists(id));
1744- assert_eq!(tree.slot(id).unwrap().tag, Tag::Unknown("carousel".to_owned()));
1745+ assert_eq!(
1746+ tree.slot(id).unwrap().tag,
1747+ Tag::Unknown("carousel".to_owned())
1748+ );
17451749 }
17461750 #[test]
17471751 fn dump_is_hiccup_of_what_the_tree_holds() {
@@ -1765,7 +1769,11 @@ mod tests {
17651769 fn dump_keeps_an_unknown_tag_and_escapes_a_string() {
17661770 let mut tree = Tree::default();
17671771 let id = tree.new_node("carousel");
1768- tree.set(id, "label", Value::Str("a \"quote\"\nand a line".to_owned()));
1772+ tree.set(
1773+ id,
1774+ "label",
1775+ Value::Str("a \"quote\"\nand a line".to_owned()),
1776+ );
17691777 assert_eq!(
17701778 tree.dump(id),
17711779 "[:carousel {:label \"a \\\"quote\\\"\\nand a line\"}]"
@@ -322,10 +322,8 @@ impl Tree {
322 if rgba.len() != expected {322 if rgba.len() != expected {
323 return false;323 return false;
324 }324 }
325- let image = egui::ColorImage::from_rgba_unmultiplied(325+ let image =
326- [width as usize, height as usize],326+ egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], rgba);
327- rgba,
328- );
329 // Overwrites whatever had not been painted yet: the newest frame is327 // Overwrites whatever had not been painted yet: the newest frame is
330 // the only one worth showing, and a backlog of stale ones is latency.328 // the only one worth showing, and a backlog of stale ones is latency.
331 self.feeds.entry(key.to_owned()).or_default().pending = Some(image);329 self.feeds.entry(key.to_owned()).or_default().pending = Some(image);
@@ -1069,7 +1067,6 @@ impl Tree {
1069 // whatever it is given, and MAX minus anything is still1067 // whatever it is given, and MAX minus anything is still
1070 // MAX — an offset the content can never reach, which left1068 // MAX — an offset the content can never reach, which left
1071 // the area painting nothing at all.1069 // the area painting nothing at all.
1072-
1073 });1070 });
1074 1071
1075 // Say when the view leaves the end and when it comes back, so1072 // Say when the view leaves the end and when it comes back, so
@@ -1091,7 +1088,11 @@ impl Tree {
1091 let end_key = key.with("at_end");1088 let end_key = key.with("at_end");
1092 let away_key = key.with("away_frames");1089 let away_key = key.with("away_frames");
1093 let away_frames = ui.ctx().data(|d| d.get_temp::<u32>(away_key)).unwrap_or(0);1090 let away_frames = ui.ctx().data(|d| d.get_temp::<u32>(away_key)).unwrap_or(0);
1094- let away_frames = if at_end { 0 } else { away_frames.saturating_add(1) };1091+ let away_frames = if at_end {
1092+ 0
1093+ } else {
1094+ away_frames.saturating_add(1)
1095+ };
1095 ui.ctx().data_mut(|d| d.insert_temp(away_key, away_frames));1096 ui.ctx().data_mut(|d| d.insert_temp(away_key, away_frames));
1096 1097
1097 let settled = if at_end {1098 let settled = if at_end {
@@ -1223,10 +1224,7 @@ impl Tree {
1223 ..1224 ..
1224 } if modifiers.command1225 } if modifiers.command
1225 )1226 )
1226- }) && !i1227+ }) && !i.events.iter().any(|e| matches!(e, egui::Event::Paste(_)))
1227- .events
1228- .iter()
1229- .any(|e| matches!(e, egui::Event::Paste(_)))
1230 });1228 });
1231 if paste_without_text {1229 if paste_without_text {
1232 self.emit(id, "paste-empty", String::new(), 0.0);1230 self.emit(id, "paste-empty", String::new(), 0.0);
@@ -1283,8 +1281,11 @@ impl Tree {
1283 .next()1281 .next()
1284 .map(|c| c.to_uppercase().to_string())1282 .map(|c| c.to_uppercase().to_string())
1285 .unwrap_or_else(|| "?".to_owned());1283 .unwrap_or_else(|| "?".to_owned());
1286- ui.painter()1284+ ui.painter().circle_filled(
1287- .circle_filled(rect.center(), size * 0.5, name_colour(&label, theme));1285+ rect.center(),
1286+ size * 0.5,
1287+ name_colour(&label, theme),
1288+ );
1288 ui.painter().text(1289 ui.painter().text(
1289 rect.center(),1290 rect.center(),
1290 Align2::CENTER_CENTER,1291 Align2::CENTER_CENTER,
@@ -1416,10 +1417,8 @@ impl Tree {
1416 return;1417 return;
1417 }1418 }
1418 let scale = (space.x / size.x).min(space.y / size.y);1419 let scale = (space.x / size.x).min(space.y / size.y);
1419- let (rect, response) =1420+ let (rect, response) = ui.allocate_exact_size(space, egui::Sense::click());
1420- ui.allocate_exact_size(space, egui::Sense::click());1421+ let painted = egui::Rect::from_center_size(rect.center(), size * scale);
1421- let painted =
1422- egui::Rect::from_center_size(rect.center(), size * scale);
1423 egui::Image::new(egui::load::SizedTexture::new(texture.id(), size * scale))1422 egui::Image::new(egui::load::SizedTexture::new(texture.id(), size * scale))
1424 .paint_at(ui, painted);1423 .paint_at(ui, painted);
1425 if response.clicked() {1424 if response.clicked() {
@@ -1483,9 +1482,7 @@ impl Tree {
1483 // otherwise for one of them. A row that sits at the bottom of a screen1482 // otherwise for one of them. A row that sits at the bottom of a screen
1484 // wants its space above it, not under it, and that is not a thing a1483 // wants its space above it, not under it, and that is not a thing a
1485 // single number can express.1484 // single number can express.
1486- let side = |key: &str| {1485+ let side = |key: &str| props.num(key, props.num("margin", 0.0)).clamp(0.0, 127.0) as i8;
1487- props.num(key, props.num("margin", 0.0)).clamp(0.0, 127.0) as i8
1488- };
1489 let margin = Margin {1486 let margin = Margin {
1490 left: side("margin-left"),1487 left: side("margin-left"),
1491 right: side("margin-right"),1488 right: side("margin-right"),
@@ -1571,7 +1568,9 @@ mod tests {
1571 let ctx = egui::Context::default();1568 let ctx = egui::Context::default();
1572 1569
1573 let mut input = egui::RawInput::default();1570 let mut input = egui::RawInput::default();
1574- input.events.push(egui::Event::PointerMoved(egui::pos2(20.0, 20.0)));1571+ input
1572+ .events
1573+ .push(egui::Event::PointerMoved(egui::pos2(20.0, 20.0)));
1575 let _ = ctx.run(input.clone(), |ctx| {1574 let _ = ctx.run(input.clone(), |ctx| {
1576 egui::CentralPanel::default().show(ctx, |ui| tree.paint(ui, &theme));1575 egui::CentralPanel::default().show(ctx, |ui| tree.paint(ui, &theme));
1577 });1576 });
@@ -1591,7 +1590,9 @@ mod tests {
1591 let card = tree.new_node("window");1590 let card = tree.new_node("window");
1592 tree.append(pill, card);1591 tree.append(pill, card);
1593 let mut input = egui::RawInput::default();1592 let mut input = egui::RawInput::default();
1594- input.events.push(egui::Event::PointerMoved(egui::pos2(20.0, 20.0)));1593+ input
1594+ .events
1595+ .push(egui::Event::PointerMoved(egui::pos2(20.0, 20.0)));
1595 let _ = ctx.run(input, |ctx| {1596 let _ = ctx.run(input, |ctx| {
1596 egui::CentralPanel::default().show(ctx, |ui| tree.paint(ui, &theme));1597 egui::CentralPanel::default().show(ctx, |ui| tree.paint(ui, &theme));
1597 });1598 });
@@ -1741,7 +1742,10 @@ mod tests {
1741 let mut tree = Tree::default();1742 let mut tree = Tree::default();
1742 let id = tree.new_node("carousel");1743 let id = tree.new_node("carousel");
1743 assert!(tree.exists(id));1744 assert!(tree.exists(id));
1744- assert_eq!(tree.slot(id).unwrap().tag, Tag::Unknown("carousel".to_owned()));1745+ assert_eq!(
1746+ tree.slot(id).unwrap().tag,
1747+ Tag::Unknown("carousel".to_owned())
1748+ );
1745 }1749 }
1746 #[test]1750 #[test]
1747 fn dump_is_hiccup_of_what_the_tree_holds() {1751 fn dump_is_hiccup_of_what_the_tree_holds() {
@@ -1765,7 +1769,11 @@ mod tests {
1765 fn dump_keeps_an_unknown_tag_and_escapes_a_string() {1769 fn dump_keeps_an_unknown_tag_and_escapes_a_string() {
1766 let mut tree = Tree::default();1770 let mut tree = Tree::default();
1767 let id = tree.new_node("carousel");1771 let id = tree.new_node("carousel");
1768- tree.set(id, "label", Value::Str("a \"quote\"\nand a line".to_owned()));1772+ tree.set(
1773+ id,
1774+ "label",
1775+ Value::Str("a \"quote\"\nand a line".to_owned()),
1776+ );
1769 assert_eq!(1777 assert_eq!(
1770 tree.dump(id),1778 tree.dump(id),
1771 "[:carousel {:label \"a \\\"quote\\\"\\nand a line\"}]"1779 "[:carousel {:label \"a \\\"quote\\\"\\nand a line\"}]"
modified crates/vidya-core/src/theme.rs +22 -30
@@ -568,45 +568,37 @@ pub fn status_dot(ui: &mut Ui, theme: &Theme, live: bool) -> Response {
568568 /// squeezed it — a status reading "joine/d", an unread count split down two
569569 /// lines — for want of the width it asked for.
570570 pub fn title(ui: &mut Ui, theme: &Theme, text: &str) {
571- ui.add(
572- Label::new(
573- RichText::new(text)
574- .size(theme.type_scale.title)
575- .strong()
576- .color(theme.palette.text),
577- ),
578- );
571+ ui.add(Label::new(
572+ RichText::new(text)
573+ .size(theme.type_scale.title)
574+ .strong()
575+ .color(theme.palette.text),
576+ ));
579577 }
580578
581579 pub fn title_2(ui: &mut Ui, theme: &Theme, text: &str) {
582- ui.add(
583- Label::new(
584- RichText::new(text)
585- .size(theme.type_scale.title_2)
586- .strong()
587- .color(theme.palette.text),
588- ),
589- );
580+ ui.add(Label::new(
581+ RichText::new(text)
582+ .size(theme.type_scale.title_2)
583+ .strong()
584+ .color(theme.palette.text),
585+ ));
590586 }
591587
592588 pub fn body(ui: &mut Ui, theme: &Theme, text: &str) {
593- ui.add(
594- Label::new(
595- RichText::new(text)
596- .size(theme.type_scale.body)
597- .color(theme.palette.text),
598- ),
599- );
589+ ui.add(Label::new(
590+ RichText::new(text)
591+ .size(theme.type_scale.body)
592+ .color(theme.palette.text),
593+ ));
600594 }
601595
602596 pub fn dim_label(ui: &mut Ui, theme: &Theme, text: &str) {
603- ui.add(
604- Label::new(
605- RichText::new(text)
606- .size(theme.type_scale.caption)
607- .color(theme.palette.text_secondary),
608- ),
609- );
597+ ui.add(Label::new(
598+ RichText::new(text)
599+ .size(theme.type_scale.caption)
600+ .color(theme.palette.text_secondary),
601+ ));
610602 }
611603
612604 /// Single-line text field with theme field padding.
@@ -568,45 +568,37 @@ pub fn status_dot(ui: &mut Ui, theme: &Theme, live: bool) -> Response {
568 /// squeezed it — a status reading "joine/d", an unread count split down two568 /// squeezed it — a status reading "joine/d", an unread count split down two
569 /// lines — for want of the width it asked for.569 /// lines — for want of the width it asked for.
570 pub fn title(ui: &mut Ui, theme: &Theme, text: &str) {570 pub fn title(ui: &mut Ui, theme: &Theme, text: &str) {
571- ui.add(571+ ui.add(Label::new(
572- Label::new(572+ RichText::new(text)
573- RichText::new(text)573+ .size(theme.type_scale.title)
574- .size(theme.type_scale.title)574+ .strong()
575- .strong()575+ .color(theme.palette.text),
576- .color(theme.palette.text),576+ ));
577- ),
578- );
579 }577 }
580 578
581 pub fn title_2(ui: &mut Ui, theme: &Theme, text: &str) {579 pub fn title_2(ui: &mut Ui, theme: &Theme, text: &str) {
582- ui.add(580+ ui.add(Label::new(
583- Label::new(581+ RichText::new(text)
584- RichText::new(text)582+ .size(theme.type_scale.title_2)
585- .size(theme.type_scale.title_2)583+ .strong()
586- .strong()584+ .color(theme.palette.text),
587- .color(theme.palette.text),585+ ));
588- ),
589- );
590 }586 }
591 587
592 pub fn body(ui: &mut Ui, theme: &Theme, text: &str) {588 pub fn body(ui: &mut Ui, theme: &Theme, text: &str) {
593- ui.add(589+ ui.add(Label::new(
594- Label::new(590+ RichText::new(text)
595- RichText::new(text)591+ .size(theme.type_scale.body)
596- .size(theme.type_scale.body)592+ .color(theme.palette.text),
597- .color(theme.palette.text),593+ ));
598- ),
599- );
600 }594 }
601 595
602 pub fn dim_label(ui: &mut Ui, theme: &Theme, text: &str) {596 pub fn dim_label(ui: &mut Ui, theme: &Theme, text: &str) {
603- ui.add(597+ ui.add(Label::new(
604- Label::new(598+ RichText::new(text)
605- RichText::new(text)599+ .size(theme.type_scale.caption)
606- .size(theme.type_scale.caption)600+ .color(theme.palette.text_secondary),
607- .color(theme.palette.text_secondary),601+ ));
608- ),
609- );
610 }602 }
611 603
612 /// Single-line text field with theme field padding.604 /// Single-line text field with theme field padding.
modified flake.nix +19 -2
@@ -125,7 +125,12 @@
125125 # skew the patch exists to prevent, rather than the empty file it is.
126126 dummySrc = craneLib.mkDummySrc {
127127 inherit src;
128+ # The stub tree already has a third-party/ of its own, so this has to
129+ # replace that directory rather than copy into it — `cp -r a b` where
130+ # b exists means b/a, and the real cpal lands at third-party/third-party
131+ # while the path cargo reads keeps the stub.
128132 extraDummyScript = ''
133+ rm -rf $out/third-party
129134 cp -r --no-preserve=mode,ownership ${src}/third-party $out/third-party
130135 '';
131136 };
@@ -141,7 +146,12 @@
141146 # cargo does not install a cdylib, so crane's default install phase —
142147 # `cargo install`, which only knows about binaries — has nothing to do.
143148 # Take the objects out of the target directory instead.
144- soPackage = { pname, package, soname }:
149+ # `dir` is named separately because a crate's directory and its cargo
150+ # package name are not the same thing here: vidya-ffi lives in
151+ # crates/jolt-vidya. Nothing is silenced — a header that stops being
152+ # there should fail the build rather than ship an object with no ABI
153+ # beside it.
154+ soPackage = { pname, package, dir, soname }:
145155 craneLib.buildPackage (commonArgs // {
146156 inherit pname cargoArtifacts;
147157 version = "0.1.0";
@@ -150,23 +160,26 @@
150160 installPhaseCommand = ''
151161 mkdir -p $out/lib $out/include
152162 cp target/release/${soname} $out/lib/
153- cp -r crates/${package}/include/. $out/include/ 2>/dev/null || true
163+ cp -r crates/${dir}/include/. $out/include/
154164 '';
155165 });
156166
157167 libvidya = soPackage {
158168 pname = "libvidya";
159169 package = "vidya-ffi";
170+ dir = "jolt-vidya";
160171 soname = "libvidya.so";
161172 };
162173 libjolttui = soPackage {
163174 pname = "libjolttui";
164175 package = "jolt-tui";
176+ dir = "jolt-tui";
165177 soname = "libjolttui.so";
166178 };
167179 libjoltmoq = soPackage {
168180 pname = "libjoltmoq";
169181 package = "jolt-moq";
182+ dir = "jolt-moq";
170183 soname = "libjoltmoq.so";
171184 };
172185
@@ -222,7 +235,11 @@
222235 cargoExtraArgs = "--locked -p vidya-ffi -p jolt-moq";
223236 } // androidEnv;
224237
238+ # Same stubbed-source caveat as the host deps above: the vendored cpal
239+ # has to survive into the dummy tree, or moq-media builds against an
240+ # empty crate here too.
225241 androidArtifacts = craneLib.buildDepsOnly (androidArgs // {
242+ inherit dummySrc;
226243 pname = "jolt-native-android-deps";
227244 version = "0.1.0";
228245 });
@@ -125,7 +125,12 @@
125 # skew the patch exists to prevent, rather than the empty file it is.125 # skew the patch exists to prevent, rather than the empty file it is.
126 dummySrc = craneLib.mkDummySrc {126 dummySrc = craneLib.mkDummySrc {
127 inherit src;127 inherit src;
128+ # The stub tree already has a third-party/ of its own, so this has to
129+ # replace that directory rather than copy into it — `cp -r a b` where
130+ # b exists means b/a, and the real cpal lands at third-party/third-party
131+ # while the path cargo reads keeps the stub.
128 extraDummyScript = ''132 extraDummyScript = ''
133+ rm -rf $out/third-party
129 cp -r --no-preserve=mode,ownership ${src}/third-party $out/third-party134 cp -r --no-preserve=mode,ownership ${src}/third-party $out/third-party
130 '';135 '';
131 };136 };
@@ -141,7 +146,12 @@
141 # cargo does not install a cdylib, so crane's default install phase —146 # cargo does not install a cdylib, so crane's default install phase —
142 # `cargo install`, which only knows about binaries — has nothing to do.147 # `cargo install`, which only knows about binaries — has nothing to do.
143 # Take the objects out of the target directory instead.148 # Take the objects out of the target directory instead.
144- soPackage = { pname, package, soname }:149+ # `dir` is named separately because a crate's directory and its cargo
150+ # package name are not the same thing here: vidya-ffi lives in
151+ # crates/jolt-vidya. Nothing is silenced — a header that stops being
152+ # there should fail the build rather than ship an object with no ABI
153+ # beside it.
154+ soPackage = { pname, package, dir, soname }:
145 craneLib.buildPackage (commonArgs // {155 craneLib.buildPackage (commonArgs // {
146 inherit pname cargoArtifacts;156 inherit pname cargoArtifacts;
147 version = "0.1.0";157 version = "0.1.0";
@@ -150,23 +160,26 @@
150 installPhaseCommand = ''160 installPhaseCommand = ''
151 mkdir -p $out/lib $out/include161 mkdir -p $out/lib $out/include
152 cp target/release/${soname} $out/lib/162 cp target/release/${soname} $out/lib/
153- cp -r crates/${package}/include/. $out/include/ 2>/dev/null || true163+ cp -r crates/${dir}/include/. $out/include/
154 '';164 '';
155 });165 });
156 166
157 libvidya = soPackage {167 libvidya = soPackage {
158 pname = "libvidya";168 pname = "libvidya";
159 package = "vidya-ffi";169 package = "vidya-ffi";
170+ dir = "jolt-vidya";
160 soname = "libvidya.so";171 soname = "libvidya.so";
161 };172 };
162 libjolttui = soPackage {173 libjolttui = soPackage {
163 pname = "libjolttui";174 pname = "libjolttui";
164 package = "jolt-tui";175 package = "jolt-tui";
176+ dir = "jolt-tui";
165 soname = "libjolttui.so";177 soname = "libjolttui.so";
166 };178 };
167 libjoltmoq = soPackage {179 libjoltmoq = soPackage {
168 pname = "libjoltmoq";180 pname = "libjoltmoq";
169 package = "jolt-moq";181 package = "jolt-moq";
182+ dir = "jolt-moq";
170 soname = "libjoltmoq.so";183 soname = "libjoltmoq.so";
171 };184 };
172 185
@@ -222,7 +235,11 @@
222 cargoExtraArgs = "--locked -p vidya-ffi -p jolt-moq";235 cargoExtraArgs = "--locked -p vidya-ffi -p jolt-moq";
223 } // androidEnv;236 } // androidEnv;
224 237
238+ # Same stubbed-source caveat as the host deps above: the vendored cpal
239+ # has to survive into the dummy tree, or moq-media builds against an
240+ # empty crate here too.
225 androidArtifacts = craneLib.buildDepsOnly (androidArgs // {241 androidArtifacts = craneLib.buildDepsOnly (androidArgs // {
242+ inherit dummySrc;
226 pname = "jolt-native-android-deps";243 pname = "jolt-native-android-deps";
227 version = "0.1.0";244 version = "0.1.0";
228 });245 });