Enable true 60fps capture and GUI repaint rate with automatic UVC high-framerate unlock

This commit is contained in:
Lucas Oskorep committed 2026-10-21 01:02:42 -04:00
1 parent 51b30cbd79
commit ef641ac31b
7 files changed
+191 -105

No files matched your search

+3 -1
View File
@@ -1006,7 +1006,9 @@ fn run(cli: Cli) -> Result<(), TransportError> {
let device_info = ctrl.get_device_info()?; let device_info = ctrl.get_device_info()?;
let ptz_version = ctrl.get_ptz_version_and_angles()?; let ptz_version = ctrl.get_ptz_version_and_angles()?;
let status = ctrl.get_device_status()?; let status = ctrl.get_device_status()?;
let (video_mode, video_mode_aux) = ctrl.get_video_mode()?; let (video_mode, video_mode_aux) = ctrl
.get_video_mode()
.unwrap_or((VideoMode::Normal, VideoModeAuxData::default()));
let extend_functions = ctrl.get_extend_function_status()?; let extend_functions = ctrl.get_extend_function_status()?;
let report = FullCameraInfoReport { let report = FullCameraInfoReport {
+35 -9
View File
@@ -86,6 +86,12 @@ impl Insta360App {
fn auto_select_insta360_video_feed(&mut self) { fn auto_select_insta360_video_feed(&mut self) {
let (w, h) = self.state.format_preset.dimensions(); let (w, h) = self.state.format_preset.dimensions();
let fps = self.state.format_preset.fps(); let fps = self.state.format_preset.fps();
if (fps > 30 || self.state.format_preset.is_vertical())
&& !self.state.vertical_resolution_mode
{
self.state.vertical_resolution_mode = true;
let _ = self.controller.set_vertical_screen(true);
}
self.video_engine self.video_engine
.set_capture_format(w as usize, h as usize, fps); .set_capture_format(w as usize, h as usize, fps);
@@ -604,7 +610,13 @@ impl Insta360App {
{ {
let (w, h) = preset.dimensions(); let (w, h) = preset.dimensions();
let fps = preset.fps(); let fps = preset.fps();
self.state.vertical_resolution_mode = preset.is_vertical(); if (fps > 30 || preset.is_vertical())
&& !self.state.vertical_resolution_mode
{
self.video_engine.stop_physical_capture();
self.state.vertical_resolution_mode = true;
let _ = self.controller.set_vertical_screen(true);
}
self.state.low_resolution_mode = w == 640; self.state.low_resolution_mode = w == 640;
self.video_engine.set_capture_format(w as usize, h as usize, fps); self.video_engine.set_capture_format(w as usize, h as usize, fps);
self.state self.state
@@ -840,7 +852,7 @@ impl Insta360App {
} }
// Live video feed rendering with aspect ratio preservation // Live video feed rendering with aspect ratio preservation
let target_aspect = if self.state.vertical_resolution_mode { let target_aspect = if self.state.format_preset.is_vertical() {
9.0_f32 / 16.0_f32 9.0_f32 / 16.0_f32
} else { } else {
16.0_f32 / 9.0_f32 16.0_f32 / 9.0_f32
@@ -2085,19 +2097,33 @@ impl Insta360App {
impl eframe::App for Insta360App { impl eframe::App for Insta360App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Continuous repaint loop for smooth 30fps live video preview // Keep capture resolution and frame rate synced with selected format preset
if self.state.preview_open {
ctx.request_repaint_after(Duration::from_millis(33));
}
// Keep capture resolution synced with selected format preset
let (target_w, target_h) = self.state.format_preset.dimensions(); let (target_w, target_h) = self.state.format_preset.dimensions();
let target_fps = self.state.format_preset.fps(); let target_fps = self.state.format_preset.fps();
if self.video_engine.capture_dimensions() != (target_w as usize, target_h as usize) { if self.video_engine.capture_dimensions() != (target_w as usize, target_h as usize)
|| self.video_engine.capture_fps() != target_fps
{
if (target_fps > 30 || self.state.format_preset.is_vertical())
&& !self.state.vertical_resolution_mode
{
self.video_engine.stop_physical_capture();
self.state.vertical_resolution_mode = true;
let _ = self.controller.set_vertical_screen(true);
}
self.video_engine self.video_engine
.set_capture_format(target_w as usize, target_h as usize, target_fps); .set_capture_format(target_w as usize, target_h as usize, target_fps);
} }
// Continuous repaint loop matching active capture framerate (16ms for 60fps, 33ms for 30fps)
if self.state.preview_open {
let repaint_ms = if self.video_engine.capture_fps() >= 50 {
16
} else {
33
};
ctx.request_repaint_after(Duration::from_millis(repaint_ms));
}
// 1. One-time hardware initialization once the physical video stream is open and delivering frames // 1. One-time hardware initialization once the physical video stream is open and delivering frames
if self.video_engine.has_live_physical_frame() && !self.state.stream_initialized { if self.video_engine.has_live_physical_frame() && !self.state.stream_initialized {
self.state.stream_initialized = true; self.state.stream_initialized = true;
+4 -4
View File
@@ -883,15 +883,15 @@ fn render_more_tab<T: UvcControlTransport>(
ui, ui,
"Portrait Resolution and High Frame Rate", "Portrait Resolution and High Frame Rate",
None, None,
Some("Enable 9:16 portrait resolution output."), Some("Unlock 60fps high frame rate and 9:16 portrait resolution in UVC."),
&mut state.vertical_resolution_mode, &mut state.vertical_resolution_mode,
!state.is_recording, !state.is_recording,
theme, theme,
) { ) {
let _ = ctrl.set_vertical_screen(state.vertical_resolution_mode); let _ = ctrl.set_vertical_screen(state.vertical_resolution_mode);
if state.vertical_resolution_mode { if !state.vertical_resolution_mode
state.format_preset = crate::state::VideoFormatPreset::Vertical1080x1920p30; && (state.format_preset.is_vertical() || state.format_preset.fps() > 30)
} else { {
state.format_preset = crate::state::VideoFormatPreset::Fhd1080p30; state.format_preset = crate::state::VideoFormatPreset::Fhd1080p30;
} }
} }
+2 -1
View File
@@ -651,7 +651,8 @@ impl CameraAppState {
self.smart_adjustment = p.smart_adjustment; self.smart_adjustment = p.smart_adjustment;
self.horizontal_correction = p.horizontal_correction; self.horizontal_correction = p.horizontal_correction;
self.roll_deg = p.roll_deg.clamp(-1.0, 1.0); self.roll_deg = p.roll_deg.clamp(-1.0, 1.0);
self.vertical_resolution_mode = p.vertical_resolution_mode; self.vertical_resolution_mode =
p.vertical_resolution_mode || p.format_preset.fps() > 30 || p.format_preset.is_vertical();
self.forced_vertical_lock = p.forced_vertical_lock; self.forced_vertical_lock = p.forced_vertical_lock;
self.low_resolution_mode = p.low_resolution_mode; self.low_resolution_mode = p.low_resolution_mode;
} }
+31 -9
View File
@@ -139,6 +139,11 @@ impl VideoPreviewEngine {
(self.capture_width, self.capture_height) (self.capture_width, self.capture_height)
} }
/// Returns the currently configured capture frame rate (`fps`).
pub fn capture_fps(&self) -> u32 {
self.capture_fps
}
/// List of discovered hardware capture devices. /// List of discovered hardware capture devices.
pub fn available_capture_devices(&self) -> &[String] { pub fn available_capture_devices(&self) -> &[String] {
&self.available_devices &self.available_devices
@@ -301,21 +306,32 @@ impl VideoPreviewEngine {
let frame_size = out_w * out_h * 3; let frame_size = out_w * out_h * 3;
let mut buffer = vec![0u8; frame_size]; let mut buffer = vec![0u8; frame_size];
for attempt in 0..2 { for attempt in 0..8 {
if stop_flag.load(Ordering::Relaxed) { if stop_flag.load(Ordering::Relaxed) {
break; break;
} }
if attempt > 0 {
std::thread::sleep( std::time::Duration::from_millis(400));
if stop_flag.load(Ordering::Relaxed) {
break;
}
}
let mut cmd = Command::new("ffmpeg"); let mut cmd = Command::new("ffmpeg");
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
// Insta360 Link AVFoundation natively supports 3840x2160, 1920x1080, 1280x720 at up to 30fps // Insta360 Link AVFoundation natively supports 3840x2160, 1920x1080, 1280x720, 1080x1920
// in '420v' (nv12) and '2vuy' (uyvy422). Using nv12 matches both the official // in '420v' (nv12) and '2vuy' (uyvy422). Using nv12 matches both the official
// Insta360 Link Controller (`OpenDevice` at 0x10008770c) and Chrome/Google Meet. // Insta360 Link Controller (`OpenDevice` at 0x10008770c) and Chrome/Google Meet.
let hw_fps = fps.min(30).to_string(); let hw_fps = if attempt < 6 {
fps.to_string()
} else {
fps.min(30).to_string()
};
let hw_size = match (out_w, out_h) { let hw_size = match (out_w, out_h) {
(3840, 2160) => "3840x2160", (3840, 2160) => "3840x2160",
(1280, 720) => "1280x720", (1280, 720) => "1280x720",
(1080, 1920) if attempt < 6 => "1080x1920",
_ => "1920x1080", _ => "1920x1080",
}; };
cmd.args([ cmd.args([
@@ -327,7 +343,7 @@ impl VideoPreviewEngine {
"-pixel_format", "-pixel_format",
"nv12", "nv12",
]); ]);
if attempt == 0 { if attempt < 7 {
cmd.args(["-framerate", &hw_fps, "-video_size", hw_size]); cmd.args(["-framerate", &hw_fps, "-video_size", hw_size]);
} else { } else {
cmd.args(["-framerate", "30", "-video_size", "1280x720"]); cmd.args(["-framerate", "30", "-video_size", "1280x720"]);
@@ -351,14 +367,19 @@ impl VideoPreviewEngine {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
let hw_fps = fps.min(30).to_string(); let hw_fps = if attempt < 6 {
fps.to_string()
} else {
fps.min(30).to_string()
};
let hw_size = match (out_w, out_h) { let hw_size = match (out_w, out_h) {
(3840, 2160) => "3840x2160", (3840, 2160) => "3840x2160",
(1280, 720) => "1280x720", (1280, 720) => "1280x720",
(1080, 1920) if attempt < 6 => "1080x1920",
_ => "1920x1080", _ => "1920x1080",
}; };
cmd.args(["-hide_banner", "-loglevel", "error", "-f", "v4l2"]); cmd.args(["-hide_banner", "-loglevel", "error", "-f", "v4l2"]);
if attempt == 0 { if attempt < 7 {
cmd.args([ cmd.args([
"-input_format", "-input_format",
"mjpeg", "mjpeg",
@@ -543,13 +564,14 @@ impl VideoPreviewEngine {
} }
} }
} else { } else {
let is_vert = state.format_preset.is_vertical();
let (out_w, out_h) = if state.low_resolution_mode { let (out_w, out_h) = if state.low_resolution_mode {
if state.vertical_resolution_mode { if is_vert {
(270, 480) (270, 480)
} else { } else {
(480, 270) (480, 270)
} }
} else if state.vertical_resolution_mode { } else if is_vert {
(360, 640) (360, 640)
} else { } else {
(640, 360) (640, 360)
@@ -1110,7 +1132,7 @@ impl VideoPreviewEngine {
let w = self.capture_width; let w = self.capture_width;
let h = self.capture_height; let h = self.capture_height;
let fps = self.capture_fps.min(30).max(15); let fps = self.capture_fps.clamp(15, 60);
let size_arg = format!("{w}x{h}"); let size_arg = format!("{w}x{h}");
let fps_arg = fps.to_string(); let fps_arg = fps.to_string();
@@ -941,6 +941,9 @@ impl<T: UvcControlTransport> Insta360CameraController<T> {
} }
let mut bits = u16::from_le_bytes([cur[0], cur[1]]); let mut bits = u16::from_le_bytes([cur[0], cur[1]]);
let mask = 1u16 << (func as u8); let mask = 1u16 << (func as u8);
if ((bits & mask) != 0) == enable {
return Ok(());
}
if enable { if enable {
bits |= mask; bits |= mask;
} else { } else {
@@ -949,11 +952,20 @@ impl<T: UvcControlTransport> Insta360CameraController<T> {
let bytes = bits.to_le_bytes(); let bytes = bits.to_le_bytes();
cur[0] = bytes[0]; cur[0] = bytes[0];
cur[1] = bytes[1]; cur[1] = bytes[1];
return self.transport.set_cur( let res = self.transport.set_cur(
self.unit_ids().extension_unit_id, self.unit_ids().extension_unit_id,
0x1b, 0x1b,
&cur, &cur,
); );
// Toggling `VerticalMode` or `LowResolution` triggers an immediate USB re-enumeration
// inside the camera firmware to expose the updated UVC frame/FPS descriptors.
if matches!(
func,
ExtendFunction::VerticalMode | ExtendFunction::LowResolution
) {
return Ok(());
}
return res;
} }
let cur = self let cur = self
.transport .transport
+103 -80
View File
@@ -101,7 +101,7 @@ pub struct NusbTransport {
device: Option<nusb::Device>, device: Option<nusb::Device>,
interface: Option<nusb::Interface>, interface: Option<nusb::Interface>,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
v4l_file: Option<std::fs::File>, v4l_file: std::sync::Mutex<Option<std::fs::File>>,
timeout: Duration, timeout: Duration,
} }
@@ -424,10 +424,35 @@ impl NusbTransport {
device, device,
interface, interface,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
v4l_file, v4l_file: std::sync::Mutex::new(v4l_file),
timeout: Duration::from_millis(1500), timeout: Duration::from_millis(1500),
}) })
} }
#[cfg(target_os = "linux")]
fn with_v4l_file<R, F>(&self, mut op: F) -> Option<R>
where
F: FnMut(&std::fs::File) -> Result<R, TransportError>,
{
let mut guard = self.v4l_file.lock().ok()?;
if let Some(ref f) = *guard {
if let Ok(res) = op(f) {
return Some(res);
}
}
// Re-open V4L2 node if the camera re-enumerated over USB (e.g., when switching
// VerticalMode / High Frame Rate UVC descriptors).
if let Some((new_f, _)) = open_linux_v4l2_node(
self.info.vendor_id,
self.info.product_id,
self.info.unit_ids.extension_unit_id,
) {
let res = op(&new_f).ok();
*guard = Some(new_f);
return res;
}
None
}
} }
impl UvcControlTransport for NusbTransport { impl UvcControlTransport for NusbTransport {
@@ -462,55 +487,55 @@ impl UvcControlTransport for NusbTransport {
}; };
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if let Some(v4l) = &self.v4l_file { if self
if is_xu { .with_v4l_file(|v4l| {
let mut buf = payload.to_vec(); if is_xu {
if linux_uvcioc_ctrl_query( let mut buf = payload.to_vec();
v4l, linux_uvcioc_ctrl_query(
unit_id, v4l,
selector, unit_id,
UvcRequest::SetCur as u8, selector,
&mut buf, UvcRequest::SetCur as u8,
) &mut buf,
.is_ok() )
} else if unit_id == self.info.unit_ids.input_terminal_id
&& selector == ct_selectors::PANTILT_ABSOLUTE
&& payload.len() >= 8
{ {
return Ok(()); let pan = i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
} let tilt = i32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]);
} else if unit_id == self.info.unit_ids.input_terminal_id let r1 = linux_v4l2_set_ctrl(v4l, V4L2_CID_PAN_ABSOLUTE, pan);
&& selector == ct_selectors::PANTILT_ABSOLUTE let r2 = linux_v4l2_set_ctrl(v4l, V4L2_CID_TILT_ABSOLUTE, tilt);
&& payload.len() >= 8 r1.or(r2)
{ } else if let Some(cid) =
let pan = i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); linux_uvc_to_v4l2_cid(&self.info.unit_ids, unit_id, selector)
let tilt = i32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]); {
let r1 = linux_v4l2_set_ctrl(v4l, V4L2_CID_PAN_ABSOLUTE, pan); let val: i32 = match payload.len() {
let r2 = linux_v4l2_set_ctrl(v4l, V4L2_CID_TILT_ABSOLUTE, tilt); 1 => payload[0] as i32,
if r1.is_ok() || r2.is_ok() { 2 => {
return Ok(()); if cid == V4L2_CID_BRIGHTNESS {
} i16::from_le_bytes([payload[0], payload[1]]) as i32
} else if let Some(cid) = } else {
linux_uvc_to_v4l2_cid(&self.info.unit_ids, unit_id, selector) u16::from_le_bytes([payload[0], payload[1]]) as i32
{ }
let val: i32 = match payload.len() {
1 => payload[0] as i32,
2 => {
if cid == V4L2_CID_BRIGHTNESS {
i16::from_le_bytes([payload[0], payload[1]]) as i32
} else {
u16::from_le_bytes([payload[0], payload[1]]) as i32
} }
} 4 => i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]),
4 => i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]), _ => 0,
_ => 0, };
}; if linux_v4l2_set_ctrl(v4l, cid, val).is_ok() {
if linux_v4l2_set_ctrl(v4l, cid, val).is_ok() {
return Ok(());
}
if cid == V4L2_CID_POWER_LINE_FREQUENCY && val == 3 {
if linux_v4l2_set_ctrl(v4l, cid, 2).is_ok() {
return Ok(()); return Ok(());
} }
if cid == V4L2_CID_POWER_LINE_FREQUENCY && val == 3 {
return linux_v4l2_set_ctrl(v4l, cid, 2);
}
Err(TransportError::Usb("V4L2 set_ctrl failed".to_string()))
} else {
Err(TransportError::Usb("unmapped V4L2 control".to_string()))
} }
} })
.is_some()
{
return Ok(());
} }
let hdr = UvcControlPacketHeader::new( let hdr = UvcControlPacketHeader::new(
@@ -567,51 +592,49 @@ impl UvcControlTransport for NusbTransport {
}; };
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if let Some(v4l) = &self.v4l_file { if let Some(out) = self.with_v4l_file(|v4l| {
if is_xu { if is_xu {
let mut buf = vec![0u8; actual_len]; let mut buf = vec![0u8; actual_len];
if linux_uvcioc_ctrl_query(v4l, unit_id, selector, req as u8, &mut buf).is_ok() { linux_uvcioc_ctrl_query(v4l, unit_id, selector, req as u8, &mut buf)?;
return Ok(buf); Ok(buf)
}
} else if unit_id == self.info.unit_ids.input_terminal_id } else if unit_id == self.info.unit_ids.input_terminal_id
&& selector == ct_selectors::PANTILT_ABSOLUTE && selector == ct_selectors::PANTILT_ABSOLUTE
&& req == UvcRequest::GetCur && req == UvcRequest::GetCur
{ {
if let (Ok(pan), Ok(tilt)) = ( let pan = linux_v4l2_get_ctrl(v4l, V4L2_CID_PAN_ABSOLUTE)?;
linux_v4l2_get_ctrl(v4l, V4L2_CID_PAN_ABSOLUTE), let tilt = linux_v4l2_get_ctrl(v4l, V4L2_CID_TILT_ABSOLUTE)?;
linux_v4l2_get_ctrl(v4l, V4L2_CID_TILT_ABSOLUTE), let mut out = vec![0u8; 8];
) { out[0..4].copy_from_slice(&pan.to_le_bytes());
let mut out = vec![0u8; 8]; out[4..8].copy_from_slice(&tilt.to_le_bytes());
out[0..4].copy_from_slice(&pan.to_le_bytes()); Ok(out)
out[4..8].copy_from_slice(&tilt.to_le_bytes());
return Ok(out);
}
} else if let Some(cid) = } else if let Some(cid) =
linux_uvc_to_v4l2_cid(&self.info.unit_ids, unit_id, selector) linux_uvc_to_v4l2_cid(&self.info.unit_ids, unit_id, selector)
{ {
let v4l_val: Result<i32, TransportError> = match req { let val: i32 = match req {
UvcRequest::GetCur => linux_v4l2_get_ctrl(v4l, cid), UvcRequest::GetCur => linux_v4l2_get_ctrl(v4l, cid)?,
UvcRequest::GetMin => linux_v4l2_query_ctrl(v4l, cid).map(|q| q.minimum), UvcRequest::GetMin => linux_v4l2_query_ctrl(v4l, cid)?.minimum,
UvcRequest::GetMax => linux_v4l2_query_ctrl(v4l, cid).map(|q| q.maximum), UvcRequest::GetMax => linux_v4l2_query_ctrl(v4l, cid)?.maximum,
UvcRequest::GetRes => linux_v4l2_query_ctrl(v4l, cid).map(|q| q.step), UvcRequest::GetRes => linux_v4l2_query_ctrl(v4l, cid)?.step,
UvcRequest::GetDef => linux_v4l2_query_ctrl(v4l, cid).map(|q| q.default_value), UvcRequest::GetDef => linux_v4l2_query_ctrl(v4l, cid)?.default_value,
_ => Err(TransportError::Usb("unsupported V4L2 request".to_string())), _ => return Err(TransportError::Usb("unsupported V4L2 request".to_string())),
}; };
if let Ok(val) = v4l_val { Ok(match actual_len {
return Ok(match actual_len { 1 => vec![val as u8],
1 => vec![val as u8], 2 => (val as i16).to_le_bytes().to_vec(),
2 => (val as i16).to_le_bytes().to_vec(), 4 => val.to_le_bytes().to_vec(),
4 => val.to_le_bytes().to_vec(), _ => {
_ => { let mut b = vec![0u8; actual_len];
let mut b = vec![0u8; actual_len]; let le = val.to_le_bytes();
let le = val.to_le_bytes(); let n = actual_len.min(4);
let n = actual_len.min(4); b[..n].copy_from_slice(&le[..n]);
b[..n].copy_from_slice(&le[..n]); b
b }
} })
}); } else {
} Err(TransportError::Usb("unmapped V4L2 control".to_string()))
} }
}) {
return Ok(out);
} }
let hdr = UvcControlPacketHeader::new( let hdr = UvcControlPacketHeader::new(