mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 02:54:26 +08:00
feat: 初步增加 Windows 支持
This commit is contained in:
@@ -22,9 +22,9 @@ use v4l2r::nix::errno::Errno;
|
||||
use v4l2r::{Format as V4l2rFormat, PixelFormat as V4l2rPixelFormat, QueueType};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::csi_bridge::{self, CsiBridgeKind, ProbeResult};
|
||||
use crate::video::device::bridge::{self as csi_bridge, CsiBridgeKind, ProbeResult};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
use crate::video::SignalStatus;
|
||||
use crate::video::signal::SignalStatus;
|
||||
|
||||
/// `io::Error` payload when the driver posts `V4L2_EVENT_SOURCE_CHANGE`.
|
||||
pub const SOURCE_CHANGED_MARKER: &str = "v4l2_source_changed";
|
||||
@@ -60,7 +60,7 @@ impl BridgeContext {
|
||||
}
|
||||
|
||||
/// V4L2 capture stream backed by v4l2r ioctl.
|
||||
pub struct V4l2rCaptureStream {
|
||||
pub struct CaptureStream {
|
||||
fd: File,
|
||||
queue: QueueType,
|
||||
resolution: Resolution,
|
||||
@@ -72,7 +72,7 @@ pub struct V4l2rCaptureStream {
|
||||
bridge_kind: Option<CsiBridgeKind>,
|
||||
}
|
||||
|
||||
impl V4l2rCaptureStream {
|
||||
impl CaptureStream {
|
||||
/// UVC: uses `resolution`. CSI bridges: DV-probe first; may return `CaptureNoSignal`.
|
||||
pub fn open(
|
||||
device_path: impl AsRef<Path>,
|
||||
@@ -538,7 +538,7 @@ impl V4l2rCaptureStream {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for V4l2rCaptureStream {
|
||||
impl Drop for CaptureStream {
|
||||
fn drop(&mut self) {
|
||||
// Release ordering matters on rkcif: a subsequent open()/S_FMT from a
|
||||
// freshly-constructed stream returns EBUSY if the previous capture has
|
||||
@@ -571,9 +571,9 @@ impl Drop for V4l2rCaptureStream {
|
||||
}
|
||||
|
||||
/// Driver-name check for CSI/HDMI bridge devices (rk_hdmirx, rkcif, tc358743,
|
||||
/// …) that expose DV timings. Kept in sync with `video::is_csi_hdmi_bridge`
|
||||
/// …) that expose DV timings. Kept in sync with `video::device::is_csi_hdmi_bridge`
|
||||
/// but queries the raw V4L2 driver string so we don't need a full
|
||||
/// `VideoDeviceInfo` at `V4l2rCaptureStream::open` time.
|
||||
/// `VideoDeviceInfo` at `CaptureStream::open` time.
|
||||
fn is_csi_bridge_driver(driver: &str) -> bool {
|
||||
let d = driver.to_ascii_lowercase();
|
||||
d == "rk_hdmirx" || d == "rkcif" || d == "tc358743" || d.starts_with("rkcif")
|
||||
12
src/video/capture/mod.rs
Normal file
12
src/video/capture/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! Video capture implementations and capture-state helpers.
|
||||
|
||||
pub(crate) mod runtime;
|
||||
pub(crate) mod status;
|
||||
|
||||
#[cfg(unix)]
|
||||
mod linux;
|
||||
#[cfg(windows)]
|
||||
#[path = "windows.rs"]
|
||||
mod linux;
|
||||
|
||||
pub use linux::*;
|
||||
70
src/video/capture/runtime.rs
Normal file
70
src/video/capture/runtime.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::video::capture::status::signal_status_from_capture_kind;
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
use crate::video::signal::SignalStatus;
|
||||
|
||||
use super::{BridgeContext, CaptureStream};
|
||||
|
||||
pub enum CaptureOpenResult {
|
||||
Opened(CaptureStream),
|
||||
NoSignal(SignalStatus),
|
||||
DeviceLost(String),
|
||||
Fatal,
|
||||
}
|
||||
|
||||
pub fn open_capture_stream(
|
||||
device_path: &Path,
|
||||
resolution: Resolution,
|
||||
format: PixelFormat,
|
||||
fps: u32,
|
||||
buffer_count: u32,
|
||||
timeout: Duration,
|
||||
bridge_ctx: BridgeContext,
|
||||
) -> Result<CaptureStream, AppError> {
|
||||
CaptureStream::open_with_bridge(
|
||||
device_path,
|
||||
resolution,
|
||||
format,
|
||||
fps,
|
||||
buffer_count.max(1),
|
||||
timeout,
|
||||
bridge_ctx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn open_capture_stream_for_retry(
|
||||
device_path: &Path,
|
||||
resolution: Resolution,
|
||||
format: PixelFormat,
|
||||
fps: u32,
|
||||
buffer_count: u32,
|
||||
timeout: Duration,
|
||||
bridge_ctx: BridgeContext,
|
||||
is_device_lost_message: impl FnOnce(&str) -> bool,
|
||||
) -> CaptureOpenResult {
|
||||
match open_capture_stream(
|
||||
device_path,
|
||||
resolution,
|
||||
format,
|
||||
fps,
|
||||
buffer_count,
|
||||
timeout,
|
||||
bridge_ctx,
|
||||
) {
|
||||
Ok(stream) => CaptureOpenResult::Opened(stream),
|
||||
Err(AppError::CaptureNoSignal { kind }) => {
|
||||
CaptureOpenResult::NoSignal(signal_status_from_capture_kind(&kind))
|
||||
}
|
||||
Err(error) => {
|
||||
let reason = error.to_string();
|
||||
if is_device_lost_message(&reason) {
|
||||
CaptureOpenResult::DeviceLost(reason)
|
||||
} else {
|
||||
CaptureOpenResult::Fatal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::io;
|
||||
|
||||
use crate::video::SignalStatus;
|
||||
use crate::video::signal::SignalStatus;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CaptureIoErrorKind {
|
||||
181
src/video/capture/windows.rs
Normal file
181
src/video/capture/windows.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::device::bridge::{CsiBridgeKind, ProbeResult};
|
||||
use crate::video::device::{directshow_display_name_from_path, normalize_windows_device_path};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
pub const SOURCE_CHANGED_MARKER: &str = "dshow_source_changed";
|
||||
|
||||
pub fn is_source_changed_error(err: &io::Error) -> bool {
|
||||
err.get_ref()
|
||||
.map(|inner| inner.to_string() == SOURCE_CHANGED_MARKER)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CaptureMeta {
|
||||
pub bytes_used: usize,
|
||||
pub sequence: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct BridgeContext {
|
||||
pub subdev_path: Option<PathBuf>,
|
||||
pub kind: Option<CsiBridgeKind>,
|
||||
}
|
||||
|
||||
impl BridgeContext {
|
||||
pub fn from_parts(subdev_path: Option<PathBuf>, kind: Option<CsiBridgeKind>) -> Self {
|
||||
Self { subdev_path, kind }
|
||||
}
|
||||
|
||||
pub fn has_subdev(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CaptureStream {
|
||||
capture: hwcodec::capture::DshowCapture,
|
||||
resolution: Resolution,
|
||||
format: PixelFormat,
|
||||
stride: u32,
|
||||
}
|
||||
|
||||
unsafe impl Send for CaptureStream {}
|
||||
|
||||
impl CaptureStream {
|
||||
pub fn open(
|
||||
device_path: impl AsRef<Path>,
|
||||
resolution: Resolution,
|
||||
format: PixelFormat,
|
||||
fps: u32,
|
||||
buffer_count: u32,
|
||||
timeout: Duration,
|
||||
) -> Result<Self> {
|
||||
let _ = buffer_count;
|
||||
let path = normalize_windows_device_path(device_path);
|
||||
let display_name = directshow_display_name_from_path(&path).ok_or_else(|| {
|
||||
AppError::VideoError(format!(
|
||||
"Unsupported DirectShow device path: {}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
let capture = hwcodec::capture::DshowCapture::open(
|
||||
&display_name,
|
||||
resolution.width as i32,
|
||||
resolution.height as i32,
|
||||
fps as i32,
|
||||
map_pixel_format(format),
|
||||
timeout.as_millis().clamp(1, i32::MAX as u128) as i32,
|
||||
)
|
||||
.map_err(|e| AppError::VideoError(format!("Failed to open DirectShow capture: {}", e)))?;
|
||||
let info = capture.info().map_err(|e| {
|
||||
AppError::VideoError(format!("Failed to query DirectShow capture: {}", e))
|
||||
})?;
|
||||
let actual_format = map_capture_format(info.pixel_format)?;
|
||||
let actual_resolution =
|
||||
Resolution::new(info.width.max(1) as u32, info.height.max(1) as u32);
|
||||
|
||||
Ok(Self {
|
||||
capture,
|
||||
resolution: actual_resolution,
|
||||
format: actual_format,
|
||||
stride: info.stride.max(0) as u32,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_with_bridge(
|
||||
device_path: impl AsRef<Path>,
|
||||
resolution: Resolution,
|
||||
format: PixelFormat,
|
||||
fps: u32,
|
||||
buffer_count: u32,
|
||||
timeout: Duration,
|
||||
bridge: BridgeContext,
|
||||
) -> Result<Self> {
|
||||
let _ = bridge;
|
||||
Self::open(device_path, resolution, format, fps, buffer_count, timeout)
|
||||
}
|
||||
|
||||
pub fn resolution(&self) -> Resolution {
|
||||
self.resolution
|
||||
}
|
||||
|
||||
pub fn format(&self) -> PixelFormat {
|
||||
self.format
|
||||
}
|
||||
|
||||
pub fn stride(&self) -> u32 {
|
||||
self.stride
|
||||
}
|
||||
|
||||
pub fn next_into(&mut self, dst: &mut Vec<u8>) -> io::Result<CaptureMeta> {
|
||||
match self.capture.read_packet() {
|
||||
Ok((packet, sequence)) => {
|
||||
dst.clear();
|
||||
dst.extend_from_slice(&packet);
|
||||
Ok(CaptureMeta {
|
||||
bytes_used: packet.len(),
|
||||
sequence,
|
||||
})
|
||||
}
|
||||
Err(err) => {
|
||||
let kind = if err.code == -110 {
|
||||
io::ErrorKind::TimedOut
|
||||
} else {
|
||||
io::ErrorKind::Other
|
||||
};
|
||||
Err(io::Error::new(kind, err.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn probe_bridge_signal_with_timeout(&self, _limit: Duration) -> Option<ProbeResult> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn map_pixel_format(format: PixelFormat) -> hwcodec::capture::CapturePixelFormat {
|
||||
match format {
|
||||
PixelFormat::Mjpeg => hwcodec::capture::CapturePixelFormat::Mjpeg,
|
||||
PixelFormat::Jpeg => hwcodec::capture::CapturePixelFormat::Jpeg,
|
||||
PixelFormat::Yuyv => hwcodec::capture::CapturePixelFormat::Yuyv,
|
||||
PixelFormat::Yvyu => hwcodec::capture::CapturePixelFormat::Yvyu,
|
||||
PixelFormat::Uyvy => hwcodec::capture::CapturePixelFormat::Uyvy,
|
||||
PixelFormat::Nv12 => hwcodec::capture::CapturePixelFormat::Nv12,
|
||||
PixelFormat::Nv21 => hwcodec::capture::CapturePixelFormat::Nv21,
|
||||
PixelFormat::Nv16 => hwcodec::capture::CapturePixelFormat::Nv16,
|
||||
PixelFormat::Nv24 => hwcodec::capture::CapturePixelFormat::Nv24,
|
||||
PixelFormat::Yuv420 => hwcodec::capture::CapturePixelFormat::Yuv420,
|
||||
PixelFormat::Yvu420 => hwcodec::capture::CapturePixelFormat::Yvu420,
|
||||
PixelFormat::Rgb24 => hwcodec::capture::CapturePixelFormat::Rgb24,
|
||||
PixelFormat::Bgr24 => hwcodec::capture::CapturePixelFormat::Bgr24,
|
||||
PixelFormat::Grey => hwcodec::capture::CapturePixelFormat::Grey,
|
||||
PixelFormat::Rgb565 => hwcodec::capture::CapturePixelFormat::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_capture_format(format: hwcodec::capture::CapturePixelFormat) -> Result<PixelFormat> {
|
||||
match format {
|
||||
hwcodec::capture::CapturePixelFormat::Mjpeg => Ok(PixelFormat::Mjpeg),
|
||||
hwcodec::capture::CapturePixelFormat::Jpeg => Ok(PixelFormat::Jpeg),
|
||||
hwcodec::capture::CapturePixelFormat::Yuyv => Ok(PixelFormat::Yuyv),
|
||||
hwcodec::capture::CapturePixelFormat::Yvyu => Ok(PixelFormat::Yvyu),
|
||||
hwcodec::capture::CapturePixelFormat::Uyvy => Ok(PixelFormat::Uyvy),
|
||||
hwcodec::capture::CapturePixelFormat::Nv12 => Ok(PixelFormat::Nv12),
|
||||
hwcodec::capture::CapturePixelFormat::Nv21 => Ok(PixelFormat::Nv21),
|
||||
hwcodec::capture::CapturePixelFormat::Nv16 => Ok(PixelFormat::Nv16),
|
||||
hwcodec::capture::CapturePixelFormat::Nv24 => Ok(PixelFormat::Nv24),
|
||||
hwcodec::capture::CapturePixelFormat::Yuv420 => Ok(PixelFormat::Yuv420),
|
||||
hwcodec::capture::CapturePixelFormat::Yvu420 => Ok(PixelFormat::Yvu420),
|
||||
hwcodec::capture::CapturePixelFormat::Rgb24 => Ok(PixelFormat::Rgb24),
|
||||
hwcodec::capture::CapturePixelFormat::Bgr24 => Ok(PixelFormat::Bgr24),
|
||||
hwcodec::capture::CapturePixelFormat::Grey => Ok(PixelFormat::Grey),
|
||||
hwcodec::capture::CapturePixelFormat::Unknown => Err(AppError::ServiceUnavailable(
|
||||
"DirectShow returned an unsupported pixel format".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
//! Shared tuning for V4L2 MJPEG capture paths (`Streamer` + `SharedVideoPipeline`).
|
||||
|
||||
/// Frames smaller than this are treated as incomplete / noise.
|
||||
pub(crate) const MIN_CAPTURE_FRAME_SIZE: usize = 128;
|
||||
|
||||
/// After startup, validate JPEG header every N frames to limit CPU use.
|
||||
pub(crate) const JPEG_VALIDATE_INTERVAL: u64 = 30;
|
||||
|
||||
/// Validate every MJPEG frame for the first N frames (UVC warm-up / bad headers).
|
||||
pub(crate) const STARTUP_JPEG_VALIDATE_FRAMES: u64 = 3;
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn should_validate_jpeg_frame(validate_counter: u64) -> bool {
|
||||
validate_counter <= STARTUP_JPEG_VALIDATE_FRAMES
|
||||
|| validate_counter.is_multiple_of(JPEG_VALIDATE_INTERVAL)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn jpeg_validation_policy_startup_then_interval() {
|
||||
assert!(should_validate_jpeg_frame(1));
|
||||
assert!(should_validate_jpeg_frame(2));
|
||||
assert!(should_validate_jpeg_frame(3));
|
||||
assert!(!should_validate_jpeg_frame(4));
|
||||
assert!(should_validate_jpeg_frame(30));
|
||||
}
|
||||
}
|
||||
299
src/video/codec/h264_bitstream.rs
Normal file
299
src/video/codec/h264_bitstream.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
//! H.264 Annex-B/AVCC bitstream helpers shared by WebRTC, RTSP and RustDesk.
|
||||
|
||||
pub const FALLBACK_WEBRTC_PROFILE_LEVEL_ID: &str = "42e01f";
|
||||
|
||||
pub fn webrtc_fmtp_line(profile_level_id: &str) -> String {
|
||||
format!(
|
||||
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id={}",
|
||||
profile_level_id
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fallback_webrtc_fmtp_line() -> String {
|
||||
webrtc_fmtp_line(FALLBACK_WEBRTC_PROFILE_LEVEL_ID)
|
||||
}
|
||||
|
||||
pub fn strip_aud_nal_units(data: &[u8]) -> Vec<u8> {
|
||||
let mut result = Vec::with_capacity(data.len());
|
||||
let mut i = 0;
|
||||
|
||||
while i < data.len() {
|
||||
let (start_code_pos, start_code_len) = if i + 4 <= data.len()
|
||||
&& data[i] == 0
|
||||
&& data[i + 1] == 0
|
||||
&& data[i + 2] == 0
|
||||
&& data[i + 3] == 1
|
||||
{
|
||||
(i, 4)
|
||||
} else if i + 3 <= data.len() && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
|
||||
(i, 3)
|
||||
} else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let nal_start = start_code_pos + start_code_len;
|
||||
if nal_start >= data.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let nal_type = data[nal_start] & 0x1F;
|
||||
|
||||
let mut nal_end = data.len();
|
||||
let mut j = nal_start + 1;
|
||||
while j + 3 <= data.len() {
|
||||
if (data[j] == 0 && data[j + 1] == 0 && data[j + 2] == 1)
|
||||
|| (j + 4 <= data.len()
|
||||
&& data[j] == 0
|
||||
&& data[j + 1] == 0
|
||||
&& data[j + 2] == 0
|
||||
&& data[j + 3] == 1)
|
||||
{
|
||||
nal_end = j;
|
||||
break;
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
|
||||
if nal_type != 9 && nal_type != 12 {
|
||||
result.extend_from_slice(&data[start_code_pos..nal_end]);
|
||||
}
|
||||
|
||||
i = nal_end;
|
||||
}
|
||||
|
||||
if result.is_empty() && !data.is_empty() {
|
||||
return data.to_vec();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn extract_sps_pps(data: &[u8]) -> (Option<Vec<u8>>, Option<Vec<u8>>) {
|
||||
let mut sps: Option<Vec<u8>> = None;
|
||||
let mut pps: Option<Vec<u8>> = None;
|
||||
let mut i = 0;
|
||||
|
||||
while i < data.len() {
|
||||
let start_code_len = if i + 4 <= data.len()
|
||||
&& data[i] == 0
|
||||
&& data[i + 1] == 0
|
||||
&& data[i + 2] == 0
|
||||
&& data[i + 3] == 1
|
||||
{
|
||||
4
|
||||
} else if i + 3 <= data.len() && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
|
||||
3
|
||||
} else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let nal_start = i + start_code_len;
|
||||
if nal_start >= data.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let nal_type = data[nal_start] & 0x1F;
|
||||
|
||||
let mut nal_end = data.len();
|
||||
let mut j = nal_start + 1;
|
||||
while j + 3 <= data.len() {
|
||||
if (data[j] == 0 && data[j + 1] == 0 && data[j + 2] == 1)
|
||||
|| (j + 4 <= data.len()
|
||||
&& data[j] == 0
|
||||
&& data[j + 1] == 0
|
||||
&& data[j + 2] == 0
|
||||
&& data[j + 3] == 1)
|
||||
{
|
||||
nal_end = j;
|
||||
break;
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
|
||||
match nal_type {
|
||||
7 => {
|
||||
sps = Some(data[nal_start..nal_end].to_vec());
|
||||
}
|
||||
8 => {
|
||||
pps = Some(data[nal_start..nal_end].to_vec());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
i = nal_end;
|
||||
}
|
||||
|
||||
(sps, pps)
|
||||
}
|
||||
|
||||
pub fn has_sps_pps(data: &[u8]) -> bool {
|
||||
let mut has_sps = false;
|
||||
let mut has_pps = false;
|
||||
let mut i = 0;
|
||||
|
||||
while i < data.len() {
|
||||
let start_code_len = if i + 4 <= data.len()
|
||||
&& data[i] == 0
|
||||
&& data[i + 1] == 0
|
||||
&& data[i + 2] == 0
|
||||
&& data[i + 3] == 1
|
||||
{
|
||||
4
|
||||
} else if i + 3 <= data.len() && data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
|
||||
3
|
||||
} else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let nal_start = i + start_code_len;
|
||||
if nal_start >= data.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let nal_type = data[nal_start] & 0x1F;
|
||||
|
||||
match nal_type {
|
||||
7 => has_sps = true,
|
||||
8 => has_pps = true,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if has_sps && has_pps {
|
||||
return true;
|
||||
}
|
||||
|
||||
i = nal_start + 1;
|
||||
}
|
||||
|
||||
has_sps && has_pps
|
||||
}
|
||||
|
||||
pub fn is_keyframe(data: &[u8]) -> bool {
|
||||
let mut i = 0;
|
||||
while i < data.len() {
|
||||
if i + 3 < data.len() && data[i] == 0 && data[i + 1] == 0 {
|
||||
let nal_start = if data[i + 2] == 1 {
|
||||
i + 3
|
||||
} else if i + 4 < data.len() && data[i + 2] == 0 && data[i + 3] == 1 {
|
||||
i + 4
|
||||
} else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
if nal_start < data.len() {
|
||||
let nal_type = data[nal_start] & 0x1F;
|
||||
if nal_type == 5 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
i = nal_start;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// `profile-level-id` hex for SDP (`42001f` etc.); expects SPS NAL without start code.
|
||||
pub fn parse_profile_level_id_from_sps(sps: &[u8]) -> Option<String> {
|
||||
if sps.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let profile_idc = sps[1];
|
||||
let constraint_set_flags = sps[2];
|
||||
let level_idc = sps[3];
|
||||
|
||||
Some(format!(
|
||||
"{:02x}{:02x}{:02x}",
|
||||
profile_idc, constraint_set_flags, level_idc
|
||||
))
|
||||
}
|
||||
|
||||
pub fn extract_profile_level_id(data: &[u8]) -> Option<String> {
|
||||
let (sps, _) = extract_sps_pps(data);
|
||||
sps.and_then(|sps_data| parse_profile_level_id_from_sps(&sps_data))
|
||||
}
|
||||
|
||||
pub fn is_annex_b(data: &[u8]) -> bool {
|
||||
data.starts_with(&[0, 0, 1]) || data.starts_with(&[0, 0, 0, 1])
|
||||
}
|
||||
|
||||
pub fn avcc_to_annex_b(data: &[u8]) -> Option<Vec<u8>> {
|
||||
let mut pos = 0;
|
||||
let mut output = Vec::with_capacity(data.len() + 16);
|
||||
let mut nalu_count = 0usize;
|
||||
|
||||
while pos + 4 <= data.len() {
|
||||
let nalu_len =
|
||||
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
if nalu_len == 0 || pos + nalu_len > data.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let nal_type = data[pos] & 0x1F;
|
||||
if nal_type != 9 && nal_type != 12 {
|
||||
output.extend_from_slice(&[0, 0, 0, 1]);
|
||||
output.extend_from_slice(&data[pos..pos + nalu_len]);
|
||||
}
|
||||
nalu_count += 1;
|
||||
pos += nalu_len;
|
||||
}
|
||||
|
||||
if pos == data.len() && nalu_count > 0 && !output.is_empty() {
|
||||
Some(output)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_for_webrtc(data: &[u8]) -> Vec<u8> {
|
||||
if is_annex_b(data) {
|
||||
return strip_aud_nal_units(data);
|
||||
}
|
||||
|
||||
if let Some(annex_b) = avcc_to_annex_b(data) {
|
||||
return strip_aud_nal_units(&annex_b);
|
||||
}
|
||||
|
||||
data.to_vec()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn detects_h264_keyframes() {
|
||||
let idr_frame = vec![0x00, 0x00, 0x00, 0x01, 0x65];
|
||||
assert!(is_keyframe(&idr_frame));
|
||||
|
||||
let idr_frame_3 = vec![0x00, 0x00, 0x01, 0x65];
|
||||
assert!(is_keyframe(&idr_frame_3));
|
||||
|
||||
let p_frame = vec![0x00, 0x00, 0x00, 0x01, 0x41];
|
||||
assert!(!is_keyframe(&p_frame));
|
||||
|
||||
let sps = vec![0x00, 0x00, 0x00, 0x01, 0x67];
|
||||
assert!(!is_keyframe(&sps));
|
||||
|
||||
let multi_nal = vec![
|
||||
0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x01, 0x68, 0xce,
|
||||
0x38, 0x80, 0x00, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84,
|
||||
];
|
||||
assert!(is_keyframe(&multi_nal));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_profile_level_id_from_sps() {
|
||||
assert_eq!(
|
||||
parse_profile_level_id_from_sps(&[0x67, 0x42, 0x40, 0x2a]),
|
||||
Some("42402a".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -347,7 +347,7 @@ impl JpegEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::video::encoder::traits::Encoder for JpegEncoder {
|
||||
impl crate::video::codec::traits::Encoder for JpegEncoder {
|
||||
fn name(&self) -> &str {
|
||||
"JPEG (libyuv+turbojpeg)"
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use hwcodec::ffmpeg_ram::decode::{DecodeContext, Decoder};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::convert::Nv12Converter;
|
||||
use crate::video::codec::convert::Nv12Converter;
|
||||
use crate::video::format::Resolution;
|
||||
|
||||
pub struct MjpegRkmppDecoder {
|
||||
@@ -1,57 +1,45 @@
|
||||
//! Video encoder implementations
|
||||
//!
|
||||
//! This module provides video encoding capabilities including:
|
||||
//! - JPEG encoding for raw frames (YUYV, NV12, etc.)
|
||||
//! - H264 encoding (hardware + software)
|
||||
//! - H265 encoding (hardware + software)
|
||||
//! - VP8 encoding (hardware + software)
|
||||
//! - VP9 encoding (hardware + software)
|
||||
//! - WebRTC video codec abstraction
|
||||
//! - Encoder registry for automatic detection
|
||||
//! Video codec, conversion, encoding, and decoding implementations.
|
||||
|
||||
use hwcodec::common::DataFormat;
|
||||
use hwcodec::ffmpeg_ram::CodecInfo;
|
||||
|
||||
pub mod codec;
|
||||
pub mod convert;
|
||||
|
||||
pub mod h264;
|
||||
pub mod h264_bitstream;
|
||||
pub mod h265;
|
||||
pub mod jpeg;
|
||||
pub mod registry;
|
||||
pub mod self_check;
|
||||
pub mod traits;
|
||||
pub mod video_codec;
|
||||
pub mod vp8;
|
||||
pub mod vp9;
|
||||
|
||||
// Core traits and types
|
||||
pub use traits::{
|
||||
BitratePreset, EncodedFormat, EncodedFrame, Encoder, EncoderConfig, EncoderFactory,
|
||||
};
|
||||
pub mod mjpeg_turbo;
|
||||
|
||||
// WebRTC codec abstraction
|
||||
pub use codec::{CodecFrame, VideoCodec, VideoCodecConfig, VideoCodecFactory, VideoCodecType};
|
||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
||||
pub mod mjpeg_rkmpp;
|
||||
|
||||
// Encoder registry
|
||||
pub use convert::{PixelConverter, Yuv420pBuffer};
|
||||
pub use h264::{H264Config, H264Encoder, H264EncoderType, H264InputFormat};
|
||||
pub use h265::{H265Config, H265Encoder, H265EncoderType, H265InputFormat};
|
||||
pub use jpeg::JpegEncoder;
|
||||
pub use mjpeg_turbo::MjpegTurboDecoder;
|
||||
pub use registry::{AvailableEncoder, EncoderBackend, EncoderRegistry, VideoEncoderType};
|
||||
pub use self_check::{
|
||||
build_hardware_self_check_runtime_error, run_hardware_self_check, VideoEncoderSelfCheckCell,
|
||||
VideoEncoderSelfCheckCodec, VideoEncoderSelfCheckResponse, VideoEncoderSelfCheckRow,
|
||||
};
|
||||
|
||||
// H264 encoder
|
||||
pub use h264::{H264Config, H264Encoder, H264EncoderType, H264InputFormat};
|
||||
|
||||
// H265 encoder
|
||||
pub use h265::{H265Config, H265Encoder, H265EncoderType, H265InputFormat};
|
||||
|
||||
// VP8 encoder
|
||||
pub use traits::{
|
||||
BitratePreset, EncodedFormat, EncodedFrame, Encoder, EncoderConfig, EncoderFactory,
|
||||
};
|
||||
pub use video_codec::{
|
||||
CodecFrame, VideoCodec, VideoCodecConfig, VideoCodecFactory, VideoCodecType,
|
||||
};
|
||||
pub use vp8::{VP8Config, VP8Encoder, VP8EncoderType, VP8InputFormat};
|
||||
|
||||
// VP9 encoder
|
||||
pub use vp9::{VP9Config, VP9Encoder, VP9EncoderType, VP9InputFormat};
|
||||
|
||||
// JPEG encoder
|
||||
pub use jpeg::JpegEncoder;
|
||||
|
||||
pub(crate) fn select_codec_for_format<F>(
|
||||
encoders: &[CodecInfo],
|
||||
format: DataFormat,
|
||||
@@ -258,7 +258,7 @@ pub trait VideoCodec: Send {
|
||||
|
||||
/// Get SDP fmtp parameters (codec-specific)
|
||||
///
|
||||
/// For H264: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f"
|
||||
/// For H264: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=<sps>"
|
||||
/// For VP8/VP9: None or empty
|
||||
fn sdp_fmtp(&self) -> Option<String>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::config::{AppConfig, RtspCodec, StreamMode};
|
||||
use crate::error::Result;
|
||||
use crate::video::encoder::registry::VideoEncoderType;
|
||||
use crate::video::encoder::VideoCodecType;
|
||||
use crate::video::codec::registry::VideoEncoderType;
|
||||
use crate::video::codec::VideoCodecType;
|
||||
use crate::video::VideoStreamManager;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
//! Video decoder implementations
|
||||
//!
|
||||
//! This module provides video decoding capabilities.
|
||||
|
||||
pub mod mjpeg_turbo;
|
||||
|
||||
pub use mjpeg_turbo::MjpegTurboDecoder;
|
||||
@@ -17,7 +17,7 @@ use v4l2r::bindings::{
|
||||
use v4l2r::ioctl::{self, Event as V4l2Event, EventType, QueryDvTimingsError, SubscribeEventFlags};
|
||||
use v4l2r::nix::errno::Errno;
|
||||
|
||||
use crate::video::SignalStatus;
|
||||
use crate::video::signal::SignalStatus;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CsiBridgeKind {
|
||||
80
src/video/device/disabled_bridge.rs
Normal file
80
src/video/device/disabled_bridge.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::video::signal::SignalStatus;
|
||||
|
||||
pub const RK628_SUBDEV_PROBE_TIMEOUT: Duration = Duration::from_millis(3000);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CsiBridgeKind {
|
||||
Rk628,
|
||||
RkHdmirx,
|
||||
Tc358743,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProbeResult {
|
||||
Locked(DvTimingsMode),
|
||||
NoCable,
|
||||
NoSync,
|
||||
OutOfRange,
|
||||
NoSignal,
|
||||
}
|
||||
|
||||
impl ProbeResult {
|
||||
pub fn as_status(&self) -> Option<SignalStatus> {
|
||||
match self {
|
||||
ProbeResult::Locked(_) => None,
|
||||
ProbeResult::NoCable => Some(SignalStatus::NoCable),
|
||||
ProbeResult::NoSync => Some(SignalStatus::NoSync),
|
||||
ProbeResult::OutOfRange => Some(SignalStatus::OutOfRange),
|
||||
ProbeResult::NoSignal => Some(SignalStatus::NoSignal),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_locked(&self) -> bool {
|
||||
matches!(self, ProbeResult::Locked(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DvTimingsMode {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub pixelclock: u64,
|
||||
pub fps: Option<f64>,
|
||||
pub raw: (),
|
||||
}
|
||||
|
||||
pub fn discover_subdev_for_video(_video_path: &Path) -> Option<(PathBuf, CsiBridgeKind)> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn open_subdev(path: &Path) -> io::Result<File> {
|
||||
File::open(path)
|
||||
}
|
||||
|
||||
pub fn probe_signal(_subdev_fd: &File, _kind: CsiBridgeKind) -> ProbeResult {
|
||||
ProbeResult::NoSignal
|
||||
}
|
||||
|
||||
pub fn probe_signal_thread_timeout(
|
||||
_subdev_fd: &File,
|
||||
_kind: CsiBridgeKind,
|
||||
_timeout: Duration,
|
||||
) -> Option<ProbeResult> {
|
||||
Some(ProbeResult::NoSignal)
|
||||
}
|
||||
|
||||
pub fn apply_dv_timings(_subdev_fd: &File, _timings: ()) {}
|
||||
|
||||
pub fn subscribe_source_change(_subdev_fd: &File) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn wait_source_change(_subdev_fd: &File, _timeout: Duration) -> io::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
@@ -16,10 +16,10 @@ use v4l2r::ioctl::{
|
||||
use v4l2r::nix::errno::Errno;
|
||||
use v4l2r::{Format as V4l2rFormat, QueueType};
|
||||
|
||||
use super::csi_bridge;
|
||||
use super::format::{PixelFormat, Resolution};
|
||||
use super::bridge as csi_bridge;
|
||||
use super::{is_rk_hdmirx_driver, is_rkcif_driver};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
/// Per-node probe limit; rkcif/RK628 ioctl chains can exceed 1s under contention.
|
||||
const DEVICE_PROBE_TIMEOUT_MS: u64 = 10_000;
|
||||
35
src/video/device/mod.rs
Normal file
35
src/video/device/mod.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
//! Video device discovery, capability probing, and platform adapters.
|
||||
|
||||
#[cfg(unix)]
|
||||
mod linux;
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub use linux::*;
|
||||
#[cfg(windows)]
|
||||
pub use windows::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod bridge;
|
||||
#[cfg(windows)]
|
||||
#[path = "disabled_bridge.rs"]
|
||||
pub mod bridge;
|
||||
|
||||
pub(crate) fn is_rk_hdmirx_driver(driver: &str, card: &str) -> bool {
|
||||
driver.eq_ignore_ascii_case("rk_hdmirx") || card.eq_ignore_ascii_case("rk_hdmirx")
|
||||
}
|
||||
|
||||
pub(crate) fn is_rk_hdmirx_device(device: &VideoDeviceInfo) -> bool {
|
||||
is_rk_hdmirx_driver(&device.driver, &device.card)
|
||||
}
|
||||
|
||||
pub(crate) fn is_rkcif_driver(driver: &str) -> bool {
|
||||
driver.eq_ignore_ascii_case("rkcif")
|
||||
}
|
||||
|
||||
/// Unified check for CSI/HDMI bridge devices (rk_hdmirx, rkcif, etc.)
|
||||
/// that require special enumeration and format-selection logic.
|
||||
pub(crate) fn is_csi_hdmi_bridge(device: &VideoDeviceInfo) -> bool {
|
||||
is_rk_hdmirx_device(device) || is_rkcif_driver(&device.driver)
|
||||
}
|
||||
359
src/video/device/windows.rs
Normal file
359
src/video/device/windows.rs
Normal file
@@ -0,0 +1,359 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VideoDeviceInfo {
|
||||
pub path: PathBuf,
|
||||
pub name: String,
|
||||
pub driver: String,
|
||||
pub bus_info: String,
|
||||
pub card: String,
|
||||
pub formats: Vec<FormatInfo>,
|
||||
pub capabilities: DeviceCapabilities,
|
||||
pub is_capture_card: bool,
|
||||
pub priority: u32,
|
||||
pub has_signal: bool,
|
||||
pub subdev_path: Option<PathBuf>,
|
||||
pub bridge_kind: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VideoDeviceRecoveryHint {
|
||||
pub path: PathBuf,
|
||||
pub name: String,
|
||||
pub driver: String,
|
||||
pub bus_info: String,
|
||||
pub card: String,
|
||||
pub is_capture_card: bool,
|
||||
}
|
||||
|
||||
impl From<&VideoDeviceInfo> for VideoDeviceRecoveryHint {
|
||||
fn from(device: &VideoDeviceInfo) -> Self {
|
||||
Self {
|
||||
path: device.path.clone(),
|
||||
name: device.name.clone(),
|
||||
driver: device.driver.clone(),
|
||||
bus_info: device.bus_info.clone(),
|
||||
card: device.card.clone(),
|
||||
is_capture_card: device.is_capture_card,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FormatInfo {
|
||||
pub format: PixelFormat,
|
||||
pub resolutions: Vec<ResolutionInfo>,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResolutionInfo {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub fps: Vec<f64>,
|
||||
}
|
||||
|
||||
impl ResolutionInfo {
|
||||
pub fn new(width: u32, height: u32, fps: Vec<f64>) -> Self {
|
||||
Self { width, height, fps }
|
||||
}
|
||||
|
||||
pub fn resolution(&self) -> Resolution {
|
||||
Resolution::new(self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct DeviceCapabilities {
|
||||
pub video_capture: bool,
|
||||
pub video_capture_mplane: bool,
|
||||
pub video_output: bool,
|
||||
pub streaming: bool,
|
||||
pub read_write: bool,
|
||||
}
|
||||
|
||||
pub struct VideoDevice {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) const DIRECTSHOW_DEVICE_PREFIX: &str = "dshow:";
|
||||
|
||||
impl VideoDevice {
|
||||
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
|
||||
let path = normalize_windows_device_path(path.as_ref());
|
||||
if enumerate_devices()?
|
||||
.iter()
|
||||
.any(|device| device.path == path)
|
||||
{
|
||||
Ok(Self { path })
|
||||
} else {
|
||||
Err(AppError::VideoError(format!(
|
||||
"Windows video device not found: {}",
|
||||
path.display()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_readonly(path: impl AsRef<Path>) -> Result<Self> {
|
||||
Self::open(path)
|
||||
}
|
||||
|
||||
pub fn info(&self) -> Result<VideoDeviceInfo> {
|
||||
enumerate_devices()?
|
||||
.into_iter()
|
||||
.find(|device| device.path == self.path)
|
||||
.ok_or_else(|| {
|
||||
AppError::VideoError(format!(
|
||||
"Windows video device not found: {}",
|
||||
self.path.display()
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_windows_device_path(path: impl AsRef<Path>) -> PathBuf {
|
||||
let raw = path.as_ref().to_string_lossy();
|
||||
if raw.eq_ignore_ascii_case("auto") {
|
||||
return find_best_device()
|
||||
.map(|device| device.path)
|
||||
.unwrap_or_else(|_| PathBuf::from(raw.as_ref()));
|
||||
}
|
||||
PathBuf::from(raw.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn directshow_display_name_from_path(path: impl AsRef<Path>) -> Option<String> {
|
||||
path.as_ref()
|
||||
.to_string_lossy()
|
||||
.strip_prefix(DIRECTSHOW_DEVICE_PREFIX)
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
pub fn enumerate_devices() -> Result<Vec<VideoDeviceInfo>> {
|
||||
let names = hwcodec::capture::list_dshow_video_devices().map_err(|e| {
|
||||
AppError::VideoError(format!("Failed to enumerate DirectShow devices: {}", e))
|
||||
})?;
|
||||
|
||||
let mut devices = names
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, name)| directshow_device_from_name(index, name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
devices.sort_by(|a, b| {
|
||||
b.priority
|
||||
.cmp(&a.priority)
|
||||
.then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
pub fn find_best_device() -> Result<VideoDeviceInfo> {
|
||||
enumerate_devices()?.into_iter().next().ok_or_else(|| {
|
||||
AppError::VideoError("No DirectShow video capture devices found".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_bridge_kind(value: Option<&str>) -> Option<super::bridge::CsiBridgeKind> {
|
||||
value.and_then(|_| None)
|
||||
}
|
||||
|
||||
pub fn select_recovery_device(
|
||||
devices: &[VideoDeviceInfo],
|
||||
hint: &VideoDeviceRecoveryHint,
|
||||
) -> Option<VideoDeviceInfo> {
|
||||
devices
|
||||
.iter()
|
||||
.find(|device| device.path == hint.path || device.bus_info == hint.bus_info)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn directshow_device_from_name(index: usize, name: String) -> VideoDeviceInfo {
|
||||
let name = if name.trim().is_empty() {
|
||||
format!("Windows Capture Device {}", index + 1)
|
||||
} else {
|
||||
name
|
||||
};
|
||||
let path = PathBuf::from(format!("{}{}", DIRECTSHOW_DEVICE_PREFIX, name));
|
||||
let formats = enumerate_directshow_formats(&name);
|
||||
let priority = score_capture_device(&name, &path.to_string_lossy(), &formats);
|
||||
|
||||
VideoDeviceInfo {
|
||||
path,
|
||||
name: name.clone(),
|
||||
driver: "directshow".to_string(),
|
||||
bus_info: name.clone(),
|
||||
card: name,
|
||||
formats,
|
||||
capabilities: DeviceCapabilities {
|
||||
video_capture: true,
|
||||
video_capture_mplane: false,
|
||||
video_output: false,
|
||||
streaming: true,
|
||||
read_write: false,
|
||||
},
|
||||
is_capture_card: true,
|
||||
priority,
|
||||
has_signal: true,
|
||||
subdev_path: None,
|
||||
bridge_kind: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn enumerate_directshow_formats(name: &str) -> Vec<FormatInfo> {
|
||||
let Ok(capabilities) = hwcodec::capture::list_dshow_device_capabilities(name) else {
|
||||
return fallback_windows_formats();
|
||||
};
|
||||
|
||||
let mut formats: Vec<FormatInfo> = Vec::new();
|
||||
for capability in capabilities {
|
||||
let Some(format) = map_capture_format(capability.format) else {
|
||||
continue;
|
||||
};
|
||||
if capability.width == 0 || capability.height == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(existing) = formats.iter_mut().find(|info| info.format == format) {
|
||||
merge_resolution(
|
||||
&mut existing.resolutions,
|
||||
capability.width,
|
||||
capability.height,
|
||||
&capability.fps,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut resolutions = Vec::new();
|
||||
merge_resolution(
|
||||
&mut resolutions,
|
||||
capability.width,
|
||||
capability.height,
|
||||
&capability.fps,
|
||||
);
|
||||
formats.push(FormatInfo {
|
||||
format,
|
||||
resolutions,
|
||||
description: format_description(format).to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
for info in &mut formats {
|
||||
info.resolutions.sort_by(|left, right| {
|
||||
b_pixels(right)
|
||||
.cmp(&b_pixels(left))
|
||||
.then_with(|| right.width.cmp(&left.width))
|
||||
.then_with(|| right.height.cmp(&left.height))
|
||||
});
|
||||
}
|
||||
formats.sort_by(|a, b| {
|
||||
b.format
|
||||
.priority()
|
||||
.cmp(&a.format.priority())
|
||||
.then_with(|| a.description.cmp(&b.description))
|
||||
});
|
||||
|
||||
if formats.is_empty() {
|
||||
fallback_windows_formats()
|
||||
} else {
|
||||
formats
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_resolution(resolutions: &mut Vec<ResolutionInfo>, width: u32, height: u32, fps: &[u32]) {
|
||||
if let Some(existing) = resolutions
|
||||
.iter_mut()
|
||||
.find(|resolution| resolution.width == width && resolution.height == height)
|
||||
{
|
||||
existing.fps.extend(fps.iter().map(|value| *value as f64));
|
||||
normalize_fps_list(&mut existing.fps);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut fps_values = fps.iter().map(|value| *value as f64).collect::<Vec<_>>();
|
||||
normalize_fps_list(&mut fps_values);
|
||||
resolutions.push(ResolutionInfo::new(width, height, fps_values));
|
||||
}
|
||||
|
||||
fn b_pixels(resolution: &ResolutionInfo) -> u32 {
|
||||
resolution.width.saturating_mul(resolution.height)
|
||||
}
|
||||
|
||||
fn map_capture_format(format: hwcodec::capture::CapturePixelFormat) -> Option<PixelFormat> {
|
||||
match format {
|
||||
hwcodec::capture::CapturePixelFormat::Mjpeg => Some(PixelFormat::Mjpeg),
|
||||
hwcodec::capture::CapturePixelFormat::Jpeg => Some(PixelFormat::Jpeg),
|
||||
hwcodec::capture::CapturePixelFormat::Yuyv => Some(PixelFormat::Yuyv),
|
||||
hwcodec::capture::CapturePixelFormat::Yvyu => Some(PixelFormat::Yvyu),
|
||||
hwcodec::capture::CapturePixelFormat::Uyvy => Some(PixelFormat::Uyvy),
|
||||
hwcodec::capture::CapturePixelFormat::Nv12 => Some(PixelFormat::Nv12),
|
||||
hwcodec::capture::CapturePixelFormat::Nv21 => Some(PixelFormat::Nv21),
|
||||
hwcodec::capture::CapturePixelFormat::Nv16 => Some(PixelFormat::Nv16),
|
||||
hwcodec::capture::CapturePixelFormat::Nv24 => Some(PixelFormat::Nv24),
|
||||
hwcodec::capture::CapturePixelFormat::Yuv420 => Some(PixelFormat::Yuv420),
|
||||
hwcodec::capture::CapturePixelFormat::Yvu420 => Some(PixelFormat::Yvu420),
|
||||
hwcodec::capture::CapturePixelFormat::Rgb24 => Some(PixelFormat::Rgb24),
|
||||
hwcodec::capture::CapturePixelFormat::Bgr24 => Some(PixelFormat::Bgr24),
|
||||
hwcodec::capture::CapturePixelFormat::Grey => Some(PixelFormat::Grey),
|
||||
hwcodec::capture::CapturePixelFormat::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_fps_list(fps_list: &mut Vec<f64>) {
|
||||
fps_list.retain(|fps| fps.is_finite() && *fps > 0.0);
|
||||
for fps in fps_list.iter_mut() {
|
||||
*fps = (*fps * 100.0).round() / 100.0;
|
||||
}
|
||||
fps_list.sort_by(|a, b| b.total_cmp(a));
|
||||
fps_list.dedup_by(|a, b| (*a - *b).abs() < 0.01);
|
||||
}
|
||||
|
||||
fn format_description(format: PixelFormat) -> &'static str {
|
||||
match format {
|
||||
PixelFormat::Mjpeg => "MJPEG",
|
||||
PixelFormat::Jpeg => "JPEG",
|
||||
PixelFormat::Yuyv => "YUYV 4:2:2",
|
||||
PixelFormat::Yvyu => "YVYU 4:2:2",
|
||||
PixelFormat::Uyvy => "UYVY 4:2:2",
|
||||
PixelFormat::Nv12 => "NV12",
|
||||
PixelFormat::Nv21 => "NV21",
|
||||
PixelFormat::Nv16 => "NV16",
|
||||
PixelFormat::Nv24 => "NV24",
|
||||
PixelFormat::Yuv420 => "YUV420",
|
||||
PixelFormat::Yvu420 => "YVU420",
|
||||
PixelFormat::Rgb565 => "RGB565",
|
||||
PixelFormat::Rgb24 => "RGB24",
|
||||
PixelFormat::Bgr24 => "BGR24",
|
||||
PixelFormat::Grey => "GREY",
|
||||
}
|
||||
}
|
||||
|
||||
fn score_capture_device(name: &str, device_id: &str, formats: &[FormatInfo]) -> u32 {
|
||||
let haystack = format!("{} {}", name, device_id).to_ascii_lowercase();
|
||||
let mut score = 50;
|
||||
|
||||
if formats
|
||||
.iter()
|
||||
.any(|format| format.format == PixelFormat::Mjpeg)
|
||||
{
|
||||
score += 25;
|
||||
}
|
||||
for keyword in ["capture", "hdmi", "uvc", "video", "usb"] {
|
||||
if haystack.contains(keyword) {
|
||||
score += 10;
|
||||
}
|
||||
}
|
||||
|
||||
score
|
||||
}
|
||||
|
||||
fn fallback_windows_formats() -> Vec<FormatInfo> {
|
||||
vec![FormatInfo {
|
||||
format: PixelFormat::Mjpeg,
|
||||
resolutions: Vec::new(),
|
||||
description: "DirectShow auto-detected stream format".to_string(),
|
||||
}]
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
#[cfg(unix)]
|
||||
use v4l2r::PixelFormat as V4l2rPixelFormat;
|
||||
|
||||
/// Supported pixel formats
|
||||
@@ -85,11 +86,13 @@ impl PixelFormat {
|
||||
}
|
||||
|
||||
/// Convert to v4l2r PixelFormat
|
||||
#[cfg(unix)]
|
||||
pub fn to_v4l2r(&self) -> V4l2rPixelFormat {
|
||||
V4l2rPixelFormat::from(&self.to_fourcc())
|
||||
}
|
||||
|
||||
/// Convert from v4l2r PixelFormat
|
||||
#[cfg(unix)]
|
||||
pub fn from_v4l2r(format: V4l2rPixelFormat) -> Option<Self> {
|
||||
let repr: [u8; 4] = format.into();
|
||||
Self::from_fourcc(repr)
|
||||
|
||||
@@ -2,81 +2,30 @@
|
||||
//!
|
||||
//! This module provides V4L2 video capture, encoding, and streaming functionality.
|
||||
|
||||
pub(crate) mod capture_limits;
|
||||
pub(crate) mod capture_status;
|
||||
pub mod capture;
|
||||
pub mod codec;
|
||||
pub mod codec_constraints;
|
||||
pub mod convert;
|
||||
pub mod csi_bridge;
|
||||
pub mod decoder;
|
||||
pub mod device;
|
||||
pub mod encoder;
|
||||
pub mod format;
|
||||
pub mod frame;
|
||||
pub mod shared_video_pipeline;
|
||||
pub mod pipeline;
|
||||
pub mod signal;
|
||||
pub mod stream_manager;
|
||||
pub mod streamer;
|
||||
pub mod traits;
|
||||
pub mod types;
|
||||
pub mod usb_reset;
|
||||
pub mod v4l2r_capture;
|
||||
|
||||
pub use convert::{PixelConverter, Yuv420pBuffer};
|
||||
pub use codec::{H264Encoder, H264EncoderType, JpegEncoder, PixelConverter, Yuv420pBuffer};
|
||||
pub use device::{VideoDevice, VideoDeviceInfo};
|
||||
pub use encoder::{H264Encoder, H264EncoderType, JpegEncoder};
|
||||
pub use format::PixelFormat;
|
||||
pub use frame::VideoFrame;
|
||||
pub use shared_video_pipeline::{
|
||||
pub use pipeline::{
|
||||
EncodedVideoFrame, SharedVideoPipeline, SharedVideoPipelineConfig, SharedVideoPipelineStats,
|
||||
};
|
||||
pub use signal::SignalStatus;
|
||||
pub use stream_manager::VideoStreamManager;
|
||||
pub use streamer::{Streamer, StreamerState};
|
||||
|
||||
/// Fine-grained signal status reported by CSI/HDMI bridge devices.
|
||||
///
|
||||
/// Only `rk_hdmirx` / `rkcif` / tc358743-class bridges can distinguish these
|
||||
/// via `VIDIOC_QUERY_DV_TIMINGS` errno; USB UVC devices always report `Ok`
|
||||
/// until they fail with a generic timeout.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SignalStatus {
|
||||
/// HDMI cable physically disconnected (`ENOLINK`).
|
||||
NoCable,
|
||||
/// TMDS signal present but timings cannot be locked (`ENOLCK`).
|
||||
NoSync,
|
||||
/// Timings outside of hardware capability (`ERANGE`).
|
||||
OutOfRange,
|
||||
/// Generic "no usable source" (fallback for EINVAL / EIO / unknown errnos).
|
||||
NoSignal,
|
||||
/// UVC/USB isochronous protocol error (common kernel: status -71 / userspace EPROTO).
|
||||
UvcUsbError,
|
||||
/// UVC capture stalled (repeated DQBUF timeouts; often cable, hub, or controller load).
|
||||
UvcCaptureStall,
|
||||
}
|
||||
|
||||
impl SignalStatus {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
SignalStatus::NoCable => "no_cable",
|
||||
SignalStatus::NoSync => "no_sync",
|
||||
SignalStatus::OutOfRange => "out_of_range",
|
||||
SignalStatus::NoSignal => "no_signal",
|
||||
SignalStatus::UvcUsbError => "uvc_usb_error",
|
||||
SignalStatus::UvcCaptureStall => "uvc_capture_stall",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
Some(match s {
|
||||
"no_cable" => SignalStatus::NoCable,
|
||||
"no_sync" => SignalStatus::NoSync,
|
||||
"out_of_range" => SignalStatus::OutOfRange,
|
||||
"no_signal" => SignalStatus::NoSignal,
|
||||
"uvc_usb_error" => SignalStatus::UvcUsbError,
|
||||
"uvc_capture_stall" => SignalStatus::UvcCaptureStall,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SignalStatus> for streamer::StreamerState {
|
||||
fn from(value: SignalStatus) -> Self {
|
||||
match value {
|
||||
@@ -89,21 +38,3 @@ impl From<SignalStatus> for streamer::StreamerState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_rk_hdmirx_driver(driver: &str, card: &str) -> bool {
|
||||
driver.eq_ignore_ascii_case("rk_hdmirx") || card.eq_ignore_ascii_case("rk_hdmirx")
|
||||
}
|
||||
|
||||
pub(crate) fn is_rk_hdmirx_device(device: &device::VideoDeviceInfo) -> bool {
|
||||
is_rk_hdmirx_driver(&device.driver, &device.card)
|
||||
}
|
||||
|
||||
pub(crate) fn is_rkcif_driver(driver: &str) -> bool {
|
||||
driver.eq_ignore_ascii_case("rkcif")
|
||||
}
|
||||
|
||||
/// Unified check for CSI/HDMI bridge devices (rk_hdmirx, rkcif, etc.)
|
||||
/// that require special enumeration and format-selection logic.
|
||||
pub(crate) fn is_csi_hdmi_bridge(device: &device::VideoDeviceInfo) -> bool {
|
||||
is_rk_hdmirx_device(device) || is_rkcif_driver(&device.driver)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::convert::{Nv12Converter, PixelConverter};
|
||||
use crate::video::decoder::MjpegTurboDecoder;
|
||||
use crate::video::encoder::h264::{H264Config, H264Encoder, H264InputFormat};
|
||||
use crate::video::encoder::h265::{H265Config, H265Encoder, H265InputFormat};
|
||||
use crate::video::encoder::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType};
|
||||
use crate::video::encoder::traits::EncoderConfig;
|
||||
use crate::video::encoder::vp8::{VP8Config, VP8Encoder};
|
||||
use crate::video::encoder::vp9::{VP9Config, VP9Encoder};
|
||||
use crate::video::codec::convert::{Nv12Converter, PixelConverter};
|
||||
use crate::video::codec::h264::{H264Config, H264Encoder, H264InputFormat};
|
||||
use crate::video::codec::h265::{H265Config, H265Encoder, H265InputFormat};
|
||||
use crate::video::codec::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType};
|
||||
use crate::video::codec::traits::EncoderConfig;
|
||||
use crate::video::codec::vp8::{VP8Config, VP8Encoder};
|
||||
use crate::video::codec::vp9::{VP9Config, VP9Encoder};
|
||||
use crate::video::codec::MjpegTurboDecoder;
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
||||
use hwcodec::ffmpeg_hw::{
|
||||
@@ -14,7 +14,7 @@ use hwcodec::ffmpeg_hw::{
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use super::SharedVideoPipelineConfig;
|
||||
use super::shared::SharedVideoPipelineConfig;
|
||||
|
||||
pub(super) struct EncoderThreadState {
|
||||
pub(super) encoder: Option<Box<dyn VideoEncoderTrait + Send>>,
|
||||
9
src/video/pipeline/mod.rs
Normal file
9
src/video/pipeline/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! Video processing pipelines.
|
||||
|
||||
mod encoder_state;
|
||||
mod shared;
|
||||
|
||||
pub use shared::{
|
||||
EncodedVideoFrame, PipelineStateNotification, SharedVideoPipeline, SharedVideoPipelineConfig,
|
||||
SharedVideoPipelineStats,
|
||||
};
|
||||
@@ -16,8 +16,6 @@
|
||||
//! Session1 Session2 Session3 ...
|
||||
//! ```
|
||||
|
||||
mod encoder_state;
|
||||
|
||||
use bytes::Bytes;
|
||||
use parking_lot::Mutex as ParkingMutex;
|
||||
use parking_lot::RwLock as ParkingRwLock;
|
||||
@@ -28,7 +26,7 @@ use std::time::{Duration, Instant};
|
||||
use tokio::sync::{mpsc, watch, Mutex, RwLock};
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use self::encoder_state::{build_encoder_state, EncoderThreadState};
|
||||
use super::encoder_state::{build_encoder_state, EncoderThreadState};
|
||||
|
||||
/// Grace period before auto-stopping pipeline when no subscribers (in seconds)
|
||||
const AUTO_STOP_GRACE_PERIOD_SECS: u64 = 3;
|
||||
@@ -41,20 +39,27 @@ const NOSIGNAL_POLL_MAX: Duration = Duration::from_secs(20);
|
||||
/// Throttle repeated encoding errors to avoid log flooding
|
||||
const ENCODE_ERROR_THROTTLE_SECS: u64 = 5;
|
||||
|
||||
static PROCESS_START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::utils::LogThrottler;
|
||||
use crate::video::capture_limits::{should_validate_jpeg_frame, MIN_CAPTURE_FRAME_SIZE};
|
||||
use crate::video::capture_status::{
|
||||
use crate::video::capture::runtime::{
|
||||
open_capture_stream, open_capture_stream_for_retry, CaptureOpenResult,
|
||||
};
|
||||
use crate::video::capture::status::{
|
||||
capture_error_log_key, classify_capture_io_error, is_device_lost_message,
|
||||
signal_status_from_capture_kind, CaptureIoErrorKind,
|
||||
};
|
||||
use crate::video::csi_bridge::{self, ProbeResult};
|
||||
use crate::video::capture::{is_source_changed_error, BridgeContext, CaptureStream};
|
||||
use crate::video::codec::h264_bitstream;
|
||||
use crate::video::codec::registry::{EncoderBackend, VideoEncoderType};
|
||||
use crate::video::device::bridge::{self as csi_bridge, ProbeResult};
|
||||
use crate::video::device::parse_bridge_kind;
|
||||
use crate::video::encoder::registry::{EncoderBackend, VideoEncoderType};
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
use crate::video::frame::{FrameBuffer, FrameBufferPool, VideoFrame};
|
||||
use crate::video::v4l2r_capture::{is_source_changed_error, BridgeContext, V4l2rCaptureStream};
|
||||
use crate::video::SignalStatus;
|
||||
use crate::video::signal::SignalStatus;
|
||||
|
||||
const MIN_CAPTURE_FRAME_SIZE: usize = 128;
|
||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
||||
use hwcodec::ffmpeg_hw::last_error_message as ffmpeg_hw_last_error;
|
||||
|
||||
@@ -122,7 +127,7 @@ pub struct SharedVideoPipelineConfig {
|
||||
/// Output codec type
|
||||
pub output_codec: VideoEncoderType,
|
||||
/// Bitrate preset (replaces raw bitrate_kbps)
|
||||
pub bitrate_preset: crate::video::encoder::BitratePreset,
|
||||
pub bitrate_preset: crate::video::codec::BitratePreset,
|
||||
/// Target FPS
|
||||
pub fps: u32,
|
||||
/// Encoder backend (None = auto select best available)
|
||||
@@ -135,7 +140,7 @@ impl Default for SharedVideoPipelineConfig {
|
||||
resolution: Resolution::HD720,
|
||||
input_format: PixelFormat::Yuyv,
|
||||
output_codec: VideoEncoderType::H264,
|
||||
bitrate_preset: crate::video::encoder::BitratePreset::Balanced,
|
||||
bitrate_preset: crate::video::codec::BitratePreset::Balanced,
|
||||
fps: 30,
|
||||
encoder_backend: None,
|
||||
}
|
||||
@@ -154,7 +159,7 @@ impl SharedVideoPipelineConfig {
|
||||
}
|
||||
|
||||
/// Create H264 config with bitrate preset
|
||||
pub fn h264(resolution: Resolution, preset: crate::video::encoder::BitratePreset) -> Self {
|
||||
pub fn h264(resolution: Resolution, preset: crate::video::codec::BitratePreset) -> Self {
|
||||
Self {
|
||||
resolution,
|
||||
output_codec: VideoEncoderType::H264,
|
||||
@@ -164,7 +169,7 @@ impl SharedVideoPipelineConfig {
|
||||
}
|
||||
|
||||
/// Create H265 config with bitrate preset
|
||||
pub fn h265(resolution: Resolution, preset: crate::video::encoder::BitratePreset) -> Self {
|
||||
pub fn h265(resolution: Resolution, preset: crate::video::codec::BitratePreset) -> Self {
|
||||
Self {
|
||||
resolution,
|
||||
output_codec: VideoEncoderType::H265,
|
||||
@@ -174,7 +179,7 @@ impl SharedVideoPipelineConfig {
|
||||
}
|
||||
|
||||
/// Create VP8 config with bitrate preset
|
||||
pub fn vp8(resolution: Resolution, preset: crate::video::encoder::BitratePreset) -> Self {
|
||||
pub fn vp8(resolution: Resolution, preset: crate::video::codec::BitratePreset) -> Self {
|
||||
Self {
|
||||
resolution,
|
||||
output_codec: VideoEncoderType::VP8,
|
||||
@@ -184,7 +189,7 @@ impl SharedVideoPipelineConfig {
|
||||
}
|
||||
|
||||
/// Create VP9 config with bitrate preset
|
||||
pub fn vp9(resolution: Resolution, preset: crate::video::encoder::BitratePreset) -> Self {
|
||||
pub fn vp9(resolution: Resolution, preset: crate::video::codec::BitratePreset) -> Self {
|
||||
Self {
|
||||
resolution,
|
||||
output_codec: VideoEncoderType::VP9,
|
||||
@@ -195,7 +200,7 @@ impl SharedVideoPipelineConfig {
|
||||
|
||||
/// Create config with legacy bitrate_kbps (for compatibility during migration)
|
||||
pub fn with_bitrate_kbps(mut self, bitrate_kbps: u32) -> Self {
|
||||
self.bitrate_preset = crate::video::encoder::BitratePreset::from_kbps(bitrate_kbps);
|
||||
self.bitrate_preset = crate::video::codec::BitratePreset::from_kbps(bitrate_kbps);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -261,6 +266,8 @@ pub struct SharedVideoPipeline {
|
||||
stats: Mutex<SharedVideoPipelineStats>,
|
||||
running: watch::Sender<bool>,
|
||||
running_rx: watch::Receiver<bool>,
|
||||
h264_profile_level_id: watch::Sender<Option<String>>,
|
||||
h264_profile_level_id_rx: watch::Receiver<Option<String>>,
|
||||
cmd_tx: ParkingRwLock<Option<tokio::sync::mpsc::UnboundedSender<PipelineCmd>>>,
|
||||
/// Fast running flag for blocking capture loop
|
||||
running_flag: AtomicBool,
|
||||
@@ -268,9 +275,9 @@ pub struct SharedVideoPipeline {
|
||||
sequence: AtomicU64,
|
||||
/// Atomic flag for keyframe request (avoids lock contention)
|
||||
keyframe_requested: AtomicBool,
|
||||
/// Pipeline start time for PTS calculation (epoch millis, 0 = not set)
|
||||
/// Uses AtomicI64 instead of Mutex for lock-free access
|
||||
pipeline_start_time_ms: AtomicI64,
|
||||
/// Pipeline start time for monotonic PTS calculation (microseconds from process start).
|
||||
/// Uses AtomicI64 instead of Mutex for lock-free access.
|
||||
pipeline_start_time_us: AtomicI64,
|
||||
pending_sync_geometry: ParkingMutex<Option<(Resolution, PixelFormat)>>,
|
||||
device_lost_reason: ParkingMutex<Option<String>>,
|
||||
state_notifier: ParkingRwLock<Option<Arc<dyn Fn(PipelineStateNotification) + Send + Sync>>>,
|
||||
@@ -365,6 +372,7 @@ impl SharedVideoPipeline {
|
||||
);
|
||||
|
||||
let (running_tx, running_rx) = watch::channel(false);
|
||||
let (h264_profile_tx, h264_profile_rx) = watch::channel(None);
|
||||
|
||||
let pipeline = Arc::new(Self {
|
||||
config: RwLock::new(config),
|
||||
@@ -372,11 +380,13 @@ impl SharedVideoPipeline {
|
||||
stats: Mutex::new(SharedVideoPipelineStats::default()),
|
||||
running: running_tx,
|
||||
running_rx,
|
||||
h264_profile_level_id: h264_profile_tx,
|
||||
h264_profile_level_id_rx: h264_profile_rx,
|
||||
cmd_tx: ParkingRwLock::new(None),
|
||||
running_flag: AtomicBool::new(false),
|
||||
sequence: AtomicU64::new(0),
|
||||
keyframe_requested: AtomicBool::new(false),
|
||||
pipeline_start_time_ms: AtomicI64::new(0),
|
||||
pipeline_start_time_us: AtomicI64::new(0),
|
||||
pending_sync_geometry: ParkingMutex::new(None),
|
||||
device_lost_reason: ParkingMutex::new(None),
|
||||
state_notifier: ParkingRwLock::new(None),
|
||||
@@ -518,6 +528,20 @@ impl SharedVideoPipeline {
|
||||
self.running_rx.clone()
|
||||
}
|
||||
|
||||
pub fn h264_profile_level_id_watch(&self) -> watch::Receiver<Option<String>> {
|
||||
self.h264_profile_level_id_rx.clone()
|
||||
}
|
||||
|
||||
fn update_h264_profile_level_id(&self, data: &[u8]) {
|
||||
let Some(profile_level_id) = h264_bitstream::extract_profile_level_id(data) else {
|
||||
return;
|
||||
};
|
||||
if self.h264_profile_level_id.borrow().as_deref() == Some(profile_level_id.as_str()) {
|
||||
return;
|
||||
}
|
||||
let _ = self.h264_profile_level_id.send(Some(profile_level_id));
|
||||
}
|
||||
|
||||
async fn broadcast_encoded(&self, frame: Arc<EncodedVideoFrame>) {
|
||||
let subscribers = {
|
||||
let guard = self.subscribers.read();
|
||||
@@ -568,7 +592,7 @@ impl SharedVideoPipeline {
|
||||
subdev_path.clone(),
|
||||
parse_bridge_kind(bridge_kind.as_deref()),
|
||||
);
|
||||
let preopened: Option<V4l2rCaptureStream> = match V4l2rCaptureStream::open_with_bridge(
|
||||
let preopened: Option<CaptureStream> = match open_capture_stream(
|
||||
&device_path,
|
||||
config.resolution,
|
||||
config.input_format,
|
||||
@@ -712,7 +736,7 @@ impl SharedVideoPipeline {
|
||||
let bridge_ctx =
|
||||
BridgeContext::from_parts(subdev_path, parse_bridge_kind(bridge_kind.as_deref()));
|
||||
std::thread::spawn(move || {
|
||||
let mut stream: Option<V4l2rCaptureStream> = None;
|
||||
let mut stream: Option<CaptureStream> = None;
|
||||
let mut initial_geometry: Option<(Resolution, PixelFormat)> = None;
|
||||
let mut resolution = config.resolution;
|
||||
let mut pixel_format = config.input_format;
|
||||
@@ -727,7 +751,7 @@ impl SharedVideoPipeline {
|
||||
stream = Some(s);
|
||||
}
|
||||
None => {
|
||||
match V4l2rCaptureStream::open_with_bridge(
|
||||
match open_capture_stream(
|
||||
&device_path,
|
||||
config.resolution,
|
||||
config.input_format,
|
||||
@@ -786,24 +810,13 @@ impl SharedVideoPipeline {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: try to (re)open the capture stream. Returns:
|
||||
/// * `Ok(Some(stream))` — opened successfully
|
||||
/// * `Ok(None)` — CaptureNoSignal, keep retrying later
|
||||
/// * `Err(())` — fatal (stop pipeline)
|
||||
enum OpenResult {
|
||||
Opened(V4l2rCaptureStream),
|
||||
NoSignal(SignalStatus),
|
||||
DeviceLost(String),
|
||||
Fatal,
|
||||
}
|
||||
|
||||
fn open_or_retry(
|
||||
device_path: &std::path::Path,
|
||||
config: &SharedVideoPipelineConfig,
|
||||
buffer_count: u32,
|
||||
bridge_ctx: BridgeContext,
|
||||
) -> OpenResult {
|
||||
match V4l2rCaptureStream::open_with_bridge(
|
||||
) -> CaptureOpenResult {
|
||||
match open_capture_stream_for_retry(
|
||||
device_path,
|
||||
config.resolution,
|
||||
config.input_format,
|
||||
@@ -811,28 +824,27 @@ impl SharedVideoPipeline {
|
||||
buffer_count.max(1),
|
||||
Duration::from_secs(2),
|
||||
bridge_ctx,
|
||||
is_device_lost_message,
|
||||
) {
|
||||
Ok(s) => OpenResult::Opened(s),
|
||||
Err(AppError::CaptureNoSignal { kind }) => {
|
||||
debug!("Capture soft-restart: still no signal ({})", kind);
|
||||
OpenResult::NoSignal(signal_status_from_capture_kind(&kind))
|
||||
CaptureOpenResult::NoSignal(status) => {
|
||||
debug!("Capture soft-restart: still no signal ({:?})", status);
|
||||
CaptureOpenResult::NoSignal(status)
|
||||
}
|
||||
Err(e) => {
|
||||
let reason = e.to_string();
|
||||
if is_device_lost_message(&reason) {
|
||||
error!("Capture device lost during soft-restart: {}", e);
|
||||
return OpenResult::DeviceLost(reason);
|
||||
}
|
||||
error!("Capture soft-restart failed: {}", e);
|
||||
OpenResult::Fatal
|
||||
CaptureOpenResult::DeviceLost(reason) => {
|
||||
error!("Capture device lost during soft-restart: {}", reason);
|
||||
CaptureOpenResult::DeviceLost(reason)
|
||||
}
|
||||
CaptureOpenResult::Fatal => {
|
||||
error!("Capture soft-restart failed");
|
||||
CaptureOpenResult::Fatal
|
||||
}
|
||||
opened => opened,
|
||||
}
|
||||
}
|
||||
|
||||
let mut no_subscribers_since: Option<Instant> = None;
|
||||
let grace_period = Duration::from_secs(AUTO_STOP_GRACE_PERIOD_SECS);
|
||||
let mut sequence: u64 = 0;
|
||||
let mut validate_counter: u64 = 0;
|
||||
let mut consecutive_timeouts: u32 = 0;
|
||||
let capture_error_throttler = LogThrottler::with_secs(5);
|
||||
let mut suppressed_capture_errors: HashMap<String, u64> = HashMap::new();
|
||||
@@ -869,7 +881,7 @@ impl SharedVideoPipeline {
|
||||
if stream.is_none() {
|
||||
match open_or_retry(&device_path, &config, buffer_count, bridge_ctx.clone())
|
||||
{
|
||||
OpenResult::Opened(new_stream) => {
|
||||
CaptureOpenResult::Opened(new_stream) => {
|
||||
let new_res = new_stream.resolution();
|
||||
let new_fmt = new_stream.format();
|
||||
let new_stride = new_stream.stride();
|
||||
@@ -945,7 +957,7 @@ impl SharedVideoPipeline {
|
||||
resolution.width, resolution.height, pixel_format, stride
|
||||
);
|
||||
}
|
||||
OpenResult::NoSignal(status) => {
|
||||
CaptureOpenResult::NoSignal(status) => {
|
||||
consecutive_timeouts = consecutive_timeouts.saturating_add(1);
|
||||
if consecutive_timeouts >= CAPTURE_TIMEOUT_STOP_THRESHOLD {
|
||||
warn!(
|
||||
@@ -966,14 +978,14 @@ impl SharedVideoPipeline {
|
||||
std::thread::sleep(Duration::from_millis(wait_ms));
|
||||
continue;
|
||||
}
|
||||
OpenResult::DeviceLost(reason) => {
|
||||
CaptureOpenResult::DeviceLost(reason) => {
|
||||
pipeline.mark_device_lost(reason);
|
||||
let _ = pipeline.running.send(false);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = frame_seq_tx.send(sequence.wrapping_add(1));
|
||||
break;
|
||||
}
|
||||
OpenResult::Fatal => {
|
||||
CaptureOpenResult::Fatal => {
|
||||
let _ = pipeline.running.send(false);
|
||||
pipeline.running_flag.store(false, Ordering::Release);
|
||||
let _ = frame_seq_tx.send(sequence.wrapping_add(1));
|
||||
@@ -1194,14 +1206,6 @@ impl SharedVideoPipeline {
|
||||
continue;
|
||||
}
|
||||
|
||||
validate_counter = validate_counter.wrapping_add(1);
|
||||
if pixel_format.is_compressed()
|
||||
&& should_validate_jpeg_frame(validate_counter)
|
||||
&& !VideoFrame::is_valid_jpeg_bytes(&owned[..frame_size])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
owned.truncate(frame_size);
|
||||
// Notify streaming only after frame validation passes —
|
||||
// stale/warm-up frames from V4L2 kernel queues can cause
|
||||
@@ -1246,27 +1250,22 @@ impl SharedVideoPipeline {
|
||||
let input_format = state.input_format;
|
||||
let raw_frame = frame.data();
|
||||
|
||||
// Calculate PTS from real capture timestamp (lock-free using AtomicI64)
|
||||
// This ensures smooth playback even when capture timing varies
|
||||
let frame_ts_ms = frame.capture_ts.elapsed().as_millis() as i64;
|
||||
// Convert Instant to a comparable value (negate elapsed to get "time since epoch")
|
||||
let current_ts_ms = -(frame_ts_ms);
|
||||
|
||||
// Try to set start time if not yet set (first frame wins)
|
||||
let start_ts = self.pipeline_start_time_ms.load(Ordering::Acquire);
|
||||
let pts_ms = if start_ts == 0 {
|
||||
// First frame - try to set the start time
|
||||
// Use compare_exchange to ensure only one thread sets it
|
||||
let _ = self.pipeline_start_time_ms.compare_exchange(
|
||||
let process_start = PROCESS_START.get_or_init(Instant::now);
|
||||
let current_ts_us = process_start.elapsed().as_micros() as i64;
|
||||
let start_ts_us = self.pipeline_start_time_us.load(Ordering::Acquire);
|
||||
let pts_ms = if start_ts_us == 0 {
|
||||
let start_ts_us = match self.pipeline_start_time_us.compare_exchange(
|
||||
0,
|
||||
current_ts_ms,
|
||||
current_ts_us,
|
||||
Ordering::AcqRel,
|
||||
Ordering::Acquire,
|
||||
);
|
||||
0 // First frame has PTS 0
|
||||
) {
|
||||
Ok(_) => current_ts_us,
|
||||
Err(existing) => existing,
|
||||
};
|
||||
current_ts_us.saturating_sub(start_ts_us) / 1000
|
||||
} else {
|
||||
// Subsequent frames: PTS = current - start
|
||||
current_ts_ms - start_ts
|
||||
current_ts_us.saturating_sub(start_ts_us) / 1000
|
||||
};
|
||||
|
||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
||||
@@ -1370,6 +1369,9 @@ impl SharedVideoPipeline {
|
||||
let encoded = frames.into_iter().next().unwrap();
|
||||
let is_keyframe = encoded.key == 1;
|
||||
let sequence = self.sequence.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if codec == VideoEncoderType::H264 {
|
||||
self.update_h264_profile_level_id(&encoded.data);
|
||||
}
|
||||
|
||||
// Debug log for H265 encoded frame
|
||||
if codec == VideoEncoderType::H265 && (is_keyframe || frame_count % 30 == 1) {
|
||||
@@ -1464,7 +1466,7 @@ impl SharedVideoPipeline {
|
||||
/// Set bitrate using preset
|
||||
pub async fn set_bitrate_preset(
|
||||
&self,
|
||||
preset: crate::video::encoder::BitratePreset,
|
||||
preset: crate::video::codec::BitratePreset,
|
||||
) -> Result<()> {
|
||||
let bitrate_kbps = preset.bitrate_kbps();
|
||||
let gop = {
|
||||
@@ -1478,7 +1480,7 @@ impl SharedVideoPipeline {
|
||||
|
||||
/// Set bitrate using raw kbps value (converts to appropriate preset)
|
||||
pub async fn set_bitrate(&self, bitrate_kbps: u32) -> Result<()> {
|
||||
let preset = crate::video::encoder::BitratePreset::from_kbps(bitrate_kbps);
|
||||
let preset = crate::video::codec::BitratePreset::from_kbps(bitrate_kbps);
|
||||
self.set_bitrate_preset(preset).await
|
||||
}
|
||||
|
||||
@@ -1549,7 +1551,7 @@ fn parse_h265_nal_types(data: &[u8]) -> Vec<(u8, usize)> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::video::encoder::BitratePreset;
|
||||
use crate::video::codec::BitratePreset;
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_config() {
|
||||
47
src/video/signal.rs
Normal file
47
src/video/signal.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! Video signal status classification.
|
||||
|
||||
/// Fine-grained signal status reported by CSI/HDMI bridge devices.
|
||||
///
|
||||
/// Only `rk_hdmirx` / `rkcif` / tc358743-class bridges can distinguish these
|
||||
/// via `VIDIOC_QUERY_DV_TIMINGS` errno; USB UVC devices always report `Ok`
|
||||
/// until they fail with a generic timeout.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SignalStatus {
|
||||
/// HDMI cable physically disconnected (`ENOLINK`).
|
||||
NoCable,
|
||||
/// TMDS signal present but timings cannot be locked (`ENOLCK`).
|
||||
NoSync,
|
||||
/// Timings outside of hardware capability (`ERANGE`).
|
||||
OutOfRange,
|
||||
/// Generic "no usable source" (fallback for EINVAL / EIO / unknown errnos).
|
||||
NoSignal,
|
||||
/// UVC/USB isochronous protocol error (common kernel: status -71 / userspace EPROTO).
|
||||
UvcUsbError,
|
||||
/// UVC capture stalled (repeated DQBUF timeouts; often cable, hub, or controller load).
|
||||
UvcCaptureStall,
|
||||
}
|
||||
|
||||
impl SignalStatus {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
SignalStatus::NoCable => "no_cable",
|
||||
SignalStatus::NoSync => "no_sync",
|
||||
SignalStatus::OutOfRange => "out_of_range",
|
||||
SignalStatus::NoSignal => "no_signal",
|
||||
SignalStatus::UvcUsbError => "uvc_usb_error",
|
||||
SignalStatus::UvcCaptureStall => "uvc_capture_stall",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
Some(match s {
|
||||
"no_cable" => SignalStatus::NoCable,
|
||||
"no_sync" => SignalStatus::NoSync,
|
||||
"out_of_range" => SignalStatus::OutOfRange,
|
||||
"no_signal" => SignalStatus::NoSignal,
|
||||
"uvc_usb_error" => SignalStatus::UvcUsbError,
|
||||
"uvc_capture_stall" => SignalStatus::UvcCaptureStall,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,8 @@ use crate::events::{EventBus, SystemEvent, VideoDeviceInfo};
|
||||
use crate::hid::HidController;
|
||||
use crate::stream::MjpegStreamHandler;
|
||||
use crate::video::codec_constraints::StreamCodecConstraints;
|
||||
use crate::video::device::is_csi_hdmi_bridge;
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
use crate::video::is_csi_hdmi_bridge;
|
||||
use crate::video::streamer::{Streamer, StreamerState, StreamerStats};
|
||||
use crate::video::traits::VideoOutput;
|
||||
|
||||
@@ -762,9 +762,7 @@ impl VideoStreamManager {
|
||||
pub async fn subscribe_encoded_frames(
|
||||
&self,
|
||||
) -> Option<
|
||||
tokio::sync::mpsc::Receiver<
|
||||
std::sync::Arc<crate::video::shared_video_pipeline::EncodedVideoFrame>,
|
||||
>,
|
||||
tokio::sync::mpsc::Receiver<std::sync::Arc<crate::video::pipeline::EncodedVideoFrame>>,
|
||||
> {
|
||||
// 1. Ensure video capture is initialized (for config discovery)
|
||||
if self.streamer.state().await == StreamerState::Uninitialized {
|
||||
@@ -803,12 +801,12 @@ impl VideoStreamManager {
|
||||
/// Get the current video encoding configuration from the shared pipeline
|
||||
pub async fn get_encoding_config(
|
||||
&self,
|
||||
) -> Option<crate::video::shared_video_pipeline::SharedVideoPipelineConfig> {
|
||||
) -> Option<crate::video::pipeline::SharedVideoPipelineConfig> {
|
||||
self.webrtc_streamer.get_pipeline_config().await
|
||||
}
|
||||
|
||||
/// Get current video codec type
|
||||
pub async fn current_video_codec(&self) -> crate::video::encoder::VideoCodecType {
|
||||
pub async fn current_video_codec(&self) -> crate::video::codec::VideoCodecType {
|
||||
self.webrtc_streamer.current_video_codec().await
|
||||
}
|
||||
|
||||
@@ -823,7 +821,7 @@ impl VideoStreamManager {
|
||||
/// before subscribing to encoded frames.
|
||||
pub async fn set_video_codec(
|
||||
&self,
|
||||
codec: crate::video::encoder::VideoCodecType,
|
||||
codec: crate::video::codec::VideoCodecType,
|
||||
) -> crate::error::Result<()> {
|
||||
self.webrtc_streamer.set_video_codec(codec).await
|
||||
}
|
||||
@@ -834,7 +832,7 @@ impl VideoStreamManager {
|
||||
/// based on client preferences.
|
||||
pub async fn set_bitrate_preset(
|
||||
&self,
|
||||
preset: crate::video::encoder::BitratePreset,
|
||||
preset: crate::video::codec::BitratePreset,
|
||||
) -> crate::error::Result<()> {
|
||||
self.webrtc_streamer.set_bitrate_preset(preset).await
|
||||
}
|
||||
@@ -908,19 +906,19 @@ impl VideoStreamManager {
|
||||
}
|
||||
|
||||
/// Convert VideoCodecType to lowercase string for frontend
|
||||
fn codec_to_string(codec: crate::video::encoder::VideoCodecType) -> String {
|
||||
fn codec_to_string(codec: crate::video::codec::VideoCodecType) -> String {
|
||||
match codec {
|
||||
crate::video::encoder::VideoCodecType::H264 => "h264".to_string(),
|
||||
crate::video::encoder::VideoCodecType::H265 => "h265".to_string(),
|
||||
crate::video::encoder::VideoCodecType::VP8 => "vp8".to_string(),
|
||||
crate::video::encoder::VideoCodecType::VP9 => "vp9".to_string(),
|
||||
crate::video::codec::VideoCodecType::H264 => "h264".to_string(),
|
||||
crate::video::codec::VideoCodecType::H265 => "h265".to_string(),
|
||||
crate::video::codec::VideoCodecType::VP8 => "vp8".to_string(),
|
||||
crate::video::codec::VideoCodecType::VP9 => "vp9".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::video::encoder::VideoCodecType;
|
||||
use crate::video::codec::VideoCodecType;
|
||||
|
||||
#[test]
|
||||
fn test_codec_to_string() {
|
||||
|
||||
@@ -11,24 +11,25 @@ use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use super::csi_bridge;
|
||||
use super::device::{
|
||||
enumerate_devices, find_best_device, parse_bridge_kind, select_recovery_device, VideoDevice,
|
||||
VideoDeviceInfo, VideoDeviceRecoveryHint,
|
||||
bridge as csi_bridge, enumerate_devices, find_best_device, is_csi_hdmi_bridge,
|
||||
parse_bridge_kind, select_recovery_device, VideoDevice, VideoDeviceInfo,
|
||||
VideoDeviceRecoveryHint,
|
||||
};
|
||||
use super::format::{PixelFormat, Resolution};
|
||||
use super::frame::{FrameBuffer, FrameBufferPool, VideoFrame};
|
||||
use super::is_csi_hdmi_bridge;
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::{EventBus, StreamDeviceLostKind, SystemEvent};
|
||||
use crate::stream::MjpegStreamHandler;
|
||||
use crate::utils::LogThrottler;
|
||||
use crate::video::capture_limits::{should_validate_jpeg_frame, MIN_CAPTURE_FRAME_SIZE};
|
||||
use crate::video::capture_status::{
|
||||
use crate::video::capture::runtime::open_capture_stream;
|
||||
use crate::video::capture::status::{
|
||||
capture_error_log_key, classify_capture_io_error, signal_status_from_capture_kind,
|
||||
CaptureIoErrorKind,
|
||||
};
|
||||
use crate::video::v4l2r_capture::{is_source_changed_error, BridgeContext, V4l2rCaptureStream};
|
||||
use crate::video::capture::{is_source_changed_error, BridgeContext, CaptureStream};
|
||||
|
||||
const MIN_CAPTURE_FRAME_SIZE: usize = 128;
|
||||
|
||||
/// Streamer configuration
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -358,10 +359,17 @@ impl Streamer {
|
||||
self.publish_event(self.current_state_event().await).await;
|
||||
|
||||
let devices = enumerate_devices()?;
|
||||
let device = devices
|
||||
.into_iter()
|
||||
.find(|d| d.path.to_string_lossy() == device_path)
|
||||
.ok_or_else(|| AppError::VideoError("Video device not found".to_string()))?;
|
||||
let device = if device_path.eq_ignore_ascii_case("auto") {
|
||||
devices
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| AppError::VideoError("No video devices found".to_string()))?
|
||||
} else {
|
||||
devices
|
||||
.into_iter()
|
||||
.find(|d| d.path.to_string_lossy() == device_path)
|
||||
.ok_or_else(|| AppError::VideoError("Video device not found".to_string()))?
|
||||
};
|
||||
|
||||
let (format, resolution) = self.resolve_capture_config(&device, format, resolution)?;
|
||||
|
||||
@@ -853,12 +861,12 @@ impl Streamer {
|
||||
// On RK628 this prevents a kernel null-pointer deref.
|
||||
if let Some(subdev_path) = bridge_ctx.subdev_path.as_ref() {
|
||||
match probe_subdev_signal(subdev_path, bridge_ctx.kind) {
|
||||
Some(crate::video::SignalStatus::NoCable)
|
||||
| Some(crate::video::SignalStatus::NoSync)
|
||||
| Some(crate::video::SignalStatus::NoSignal)
|
||||
| Some(crate::video::SignalStatus::OutOfRange) => {
|
||||
Some(crate::video::signal::SignalStatus::NoCable)
|
||||
| Some(crate::video::signal::SignalStatus::NoSync)
|
||||
| Some(crate::video::signal::SignalStatus::NoSignal)
|
||||
| Some(crate::video::signal::SignalStatus::OutOfRange) => {
|
||||
let status = probe_subdev_signal(subdev_path, bridge_ctx.kind)
|
||||
.unwrap_or(crate::video::SignalStatus::NoSignal);
|
||||
.unwrap_or(crate::video::signal::SignalStatus::NoSignal);
|
||||
let wait_secs = backoff_secs(no_signal_restart_count);
|
||||
debug!(
|
||||
"Pre-STREAMON gate: subdev {:?} reports {:?} — \
|
||||
@@ -884,7 +892,7 @@ impl Streamer {
|
||||
}
|
||||
|
||||
// ── Open the capture stream ─────────────────────────────────────────
|
||||
let mut stream_opt: Option<V4l2rCaptureStream> = None;
|
||||
let mut stream_opt: Option<CaptureStream> = None;
|
||||
let mut last_error: Option<String> = None;
|
||||
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
@@ -893,7 +901,7 @@ impl Streamer {
|
||||
return;
|
||||
}
|
||||
|
||||
match V4l2rCaptureStream::open_with_bridge(
|
||||
match open_capture_stream(
|
||||
&device_path,
|
||||
config.resolution,
|
||||
config.format,
|
||||
@@ -985,7 +993,6 @@ impl Streamer {
|
||||
|
||||
let buffer_pool = Arc::new(FrameBufferPool::new(BUFFER_COUNT.max(4) as usize));
|
||||
let mut signal_present = true;
|
||||
let mut validate_counter: u64 = 0;
|
||||
let mut idle_since: Option<std::time::Instant> = None;
|
||||
|
||||
let mut fps_frame_count: u64 = 0;
|
||||
@@ -1091,7 +1098,7 @@ impl Streamer {
|
||||
break 'capture;
|
||||
}
|
||||
CaptureIoErrorKind::TransientSignal { status } => {
|
||||
if status == Some(crate::video::SignalStatus::UvcUsbError) {
|
||||
if status == Some(crate::video::signal::SignalStatus::UvcUsbError) {
|
||||
warn!(
|
||||
"Capture transient error (EPROTO/-71, often UVC USB): {}",
|
||||
e
|
||||
@@ -1145,14 +1152,6 @@ impl Streamer {
|
||||
continue 'capture;
|
||||
}
|
||||
|
||||
validate_counter = validate_counter.wrapping_add(1);
|
||||
if pixel_format.is_compressed()
|
||||
&& should_validate_jpeg_frame(validate_counter)
|
||||
&& !VideoFrame::is_valid_jpeg_bytes(&owned[..frame_size])
|
||||
{
|
||||
continue 'capture;
|
||||
}
|
||||
|
||||
owned.truncate(frame_size);
|
||||
let frame = VideoFrame::from_pooled(
|
||||
Arc::new(FrameBuffer::new(owned, Some(buffer_pool.clone()))),
|
||||
@@ -1275,7 +1274,7 @@ impl Streamer {
|
||||
// Reset no_signal_since so the back-off timer is fresh for the new session.
|
||||
// no_signal_since will be re-set if the new session immediately times out.
|
||||
|
||||
// Continue 'session → re-open V4l2rCaptureStream with updated config.
|
||||
// Continue 'session → re-open CaptureStream with updated config.
|
||||
} // 'session
|
||||
|
||||
self.direct_active.store(false, Ordering::SeqCst);
|
||||
@@ -1580,7 +1579,7 @@ pub struct StreamerStats {
|
||||
fn probe_subdev_signal(
|
||||
subdev_path: &std::path::Path,
|
||||
kind: Option<csi_bridge::CsiBridgeKind>,
|
||||
) -> Option<crate::video::SignalStatus> {
|
||||
) -> Option<crate::video::signal::SignalStatus> {
|
||||
let fd = match csi_bridge::open_subdev(subdev_path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
@@ -1588,7 +1587,7 @@ fn probe_subdev_signal(
|
||||
"probe_subdev_signal: failed to open {:?}: {}",
|
||||
subdev_path, e
|
||||
);
|
||||
return Some(crate::video::SignalStatus::NoSignal);
|
||||
return Some(crate::video::signal::SignalStatus::NoSignal);
|
||||
}
|
||||
};
|
||||
let kind = kind.unwrap_or(csi_bridge::CsiBridgeKind::Unknown);
|
||||
|
||||
@@ -9,14 +9,14 @@ pub use super::format::{PixelFormat, Resolution};
|
||||
// From video::frame
|
||||
pub use super::frame::VideoFrame;
|
||||
|
||||
// From video::encoder (codec-level types)
|
||||
pub use super::encoder::{BitratePreset, VideoCodecType};
|
||||
// From video::codec (codec-level types)
|
||||
pub use super::codec::{BitratePreset, VideoCodecType};
|
||||
|
||||
// From video::encoder::registry
|
||||
pub use super::encoder::registry::{EncoderBackend, VideoEncoderType};
|
||||
// From video::codec::registry
|
||||
pub use super::codec::registry::{EncoderBackend, VideoEncoderType};
|
||||
|
||||
// From video::shared_video_pipeline
|
||||
pub use super::shared_video_pipeline::{
|
||||
// From video::pipeline
|
||||
pub use super::pipeline::{
|
||||
EncodedVideoFrame, PipelineStateNotification, SharedVideoPipeline, SharedVideoPipelineConfig,
|
||||
SharedVideoPipelineStats,
|
||||
};
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
//! USB device enumeration and reset via sysfs `authorized`.
|
||||
//!
|
||||
//! Provides APIs for the settings page to list and reset USB devices.
|
||||
//! Requires write access to `/sys/bus/usb/devices/.../authorized` (typically root).
|
||||
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Walk up from a V4L sysfs `device` link until we find a USB device node
|
||||
/// (`busnum` + `devnum` present).
|
||||
fn usb_device_dir_for_v4l_sysfs(device_link: &Path) -> io::Result<PathBuf> {
|
||||
let mut p = device_link.canonicalize()?;
|
||||
loop {
|
||||
if p.join("busnum").is_file() && p.join("devnum").is_file() {
|
||||
return Ok(p);
|
||||
}
|
||||
p = p
|
||||
.parent()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no USB parent in sysfs"))?
|
||||
.to_path_buf();
|
||||
if p.as_os_str().is_empty() || p == Path::new("/") {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"reached sysfs root without USB device",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// USB device enumeration & reset-by-bus/dev (for the settings API)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Information about a single USB device, read from `/sys/bus/usb/devices/`.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UsbDeviceInfo {
|
||||
/// USB bus number (`busnum` sysfs attribute).
|
||||
pub bus_num: u32,
|
||||
/// USB device number on the bus (`devnum` sysfs attribute).
|
||||
pub dev_num: u32,
|
||||
/// Vendor ID hex string, e.g. `"1d6b"`.
|
||||
pub id_vendor: String,
|
||||
/// Product ID hex string, e.g. `"0002"`.
|
||||
pub id_product: String,
|
||||
/// Product name from sysfs `product`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub product: Option<String>,
|
||||
/// Manufacturer name from sysfs `manufacturer`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manufacturer: Option<String>,
|
||||
/// Speed in Mbps from sysfs `speed`, e.g. `"480"`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub speed: Option<String>,
|
||||
/// `true` if authorized=1, `false` if authorized=0, `None` if no file.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub authorized: Option<bool>,
|
||||
/// Kernel driver bound to this device (from driver symlink).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub driver: Option<String>,
|
||||
/// Associated `/dev/videoN` node, if this USB device has a V4L2 child.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub video_device: Option<String>,
|
||||
}
|
||||
|
||||
/// Read a sysfs string attribute, trimming trailing newline.
|
||||
fn read_sysfs_str(dir: &Path, attr: &str) -> Option<String> {
|
||||
std::fs::read_to_string(dir.join(attr))
|
||||
.ok()
|
||||
.map(|s| s.trim_end().to_string())
|
||||
}
|
||||
|
||||
/// Read a sysfs u32 attribute.
|
||||
fn read_sysfs_u32(dir: &Path, attr: &str) -> Option<u32> {
|
||||
read_sysfs_str(dir, attr).and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Build a map from USB sysfs dir → video device name by scanning
|
||||
/// `/sys/class/video4linux/`.
|
||||
fn build_usb_to_video_map() -> std::collections::HashMap<String, String> {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
let v4l_class = Path::new("/sys/class/video4linux");
|
||||
let entries = match std::fs::read_dir(v4l_class) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return map,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let name_str = match name.to_str() {
|
||||
Some(s) if s.starts_with("video") => s,
|
||||
_ => continue,
|
||||
};
|
||||
// Resolve the device symlink and walk up to find the USB parent
|
||||
let device_link = v4l_class.join(name_str).join("device");
|
||||
if let Ok(usb_dir) = usb_device_dir_for_v4l_sysfs(&device_link) {
|
||||
if let Some(key) = usb_dir.file_name().and_then(|k| k.to_str()) {
|
||||
map.insert(key.to_string(), format!("/dev/{}", name_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// List all USB devices visible in `/sys/bus/usb/devices/`.
|
||||
pub fn list_usb_devices() -> Vec<UsbDeviceInfo> {
|
||||
let usb_bus = Path::new("/sys/bus/usb/devices");
|
||||
let entries = match std::fs::read_dir(usb_bus) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return vec![],
|
||||
};
|
||||
|
||||
let video_map = build_usb_to_video_map();
|
||||
|
||||
let mut devices: Vec<UsbDeviceInfo> = entries
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
let dir = entry.path();
|
||||
// Only consider entries that have busnum + devnum (actual devices, not interfaces)
|
||||
let bus_num = read_sysfs_u32(&dir, "busnum")?;
|
||||
let dev_num = read_sysfs_u32(&dir, "devnum")?;
|
||||
|
||||
let id_vendor = read_sysfs_str(&dir, "idVendor").unwrap_or_default();
|
||||
let id_product = read_sysfs_str(&dir, "idProduct").unwrap_or_default();
|
||||
|
||||
let product = read_sysfs_str(&dir, "product");
|
||||
let manufacturer = read_sysfs_str(&dir, "manufacturer");
|
||||
let speed = read_sysfs_str(&dir, "speed");
|
||||
|
||||
let authorized = if dir.join("authorized").exists() {
|
||||
read_sysfs_str(&dir, "authorized")
|
||||
.and_then(|s| s.trim().parse::<u8>().ok())
|
||||
.map(|v| v != 0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let driver = std::fs::read_link(dir.join("driver"))
|
||||
.ok()
|
||||
.and_then(|p| p.file_name().map(|f| f.to_string_lossy().to_string()));
|
||||
|
||||
let dir_name = dir.file_name()?.to_str()?.to_string();
|
||||
let video_device = video_map.get(&dir_name).cloned();
|
||||
|
||||
Some(UsbDeviceInfo {
|
||||
bus_num,
|
||||
dev_num,
|
||||
id_vendor,
|
||||
id_product,
|
||||
product,
|
||||
manufacturer,
|
||||
speed,
|
||||
authorized,
|
||||
driver,
|
||||
video_device,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort by bus, then device number for stable ordering.
|
||||
devices.sort_by(|a, b| (a.bus_num, a.dev_num).cmp(&(b.bus_num, b.dev_num)));
|
||||
devices
|
||||
}
|
||||
|
||||
/// Reset a USB device identified by bus/dev numbers via the `authorized` sysfs
|
||||
/// attribute. After re-authorizing, waits for the device to reappear.
|
||||
pub fn reset_usb_device(bus_num: u32, dev_num: u32) -> io::Result<()> {
|
||||
let usb_bus = Path::new("/sys/bus/usb/devices");
|
||||
let entries = std::fs::read_dir(usb_bus)?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let dir = entry.path();
|
||||
if read_sysfs_u32(&dir, "busnum") != Some(bus_num)
|
||||
|| read_sysfs_u32(&dir, "devnum") != Some(dev_num)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let authorized = dir.join("authorized");
|
||||
if !authorized.exists() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("device {bus_num}-{dev_num} has no authorized attribute"),
|
||||
));
|
||||
}
|
||||
std::fs::write(&authorized, b"0")?;
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
std::fs::write(&authorized, b"1")?;
|
||||
|
||||
// Wait for device to reappear
|
||||
let wait_until = Instant::now() + Duration::from_secs(2);
|
||||
while !dir.join("busnum").exists() {
|
||||
if Instant::now() >= wait_until {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("USB device {bus_num}-{dev_num} not found in sysfs"),
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user