Communitygithub.com

shiguredo/shiguredo-video-device

時雨堂的跨平台相機擷取函式庫 shiguredo_video_device 的功能與 API 參考文件。用於 macOS (AVFoundation) / Linux (V4L2 / PipeWire) / Windows (Media Foundation) 的裝置列舉與幀擷取、透過 feature 選擇後端、像素格式與幀的記憶體佈局、幀回呼的限制,以及各後端之間行為差異的相關問題。

shiguredo-video-device 是什麼?

此技能提供了跨平台相機擷取函式庫 shiguredo_video_device 的完整技術參考,支援 macOS (AVFoundation)、Linux (V4L2 / PipeWire) 以及 Windows (Media Foundation)。開發者能夠使用統一的 Rust API 來列舉攝影機裝置、設定擷取參數(如解析度、幀率、像素格式),並透過回呼機制接收視訊幀。函式庫透過 Cargo feature 來選擇與啟用特定平台的後端,並詳細說明了各後端在裝置列表、幀格式支援(NV12、YUY2、I420、MJPEG)、記憶體佈局(含 stride 處理)、以及執行緒安全性(Send/Sync 實作)上的差異與限制。此文件亦包含實用的程式碼範例,例如透過通道有效傳遞幀、處理 stride 以產生連續緩衝區,以及錯誤處理模式。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/shiguredo/video-device-rs/tree/HEAD/skills/shiguredo-video-device

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

shiguredo_video_device

一個能夠在 macOS / Linux / Windows 上,透過跨平台 Rust API 進行相機列舉與幀擷取的函式庫。

特色

  • 跨平台支援: 透過統一的 API 操作 macOS (AVFoundation)、Linux (V4L2 / PipeWire)、Windows (Media Foundation)
  • 透過 feature 選擇後端: 使用 feature 來選擇啟用的後端與預設後端
  • 回呼驅動: 在擷取過程中,後端執行緒會針對每個幀觸發回呼函式
  • 像素格式: NV12 / YUY2 / I420、Linux (V4L2) 支援 MJPEG 直通(使用 mjpeg feature)
  • 低相依性: Rust 的相依 crate 僅有 Windows (mf feature) 使用的 windows crate
  • CVPixelBuffer 參考 (macOS): 幀可以攜帶對 CVPixelBuffer 的參考 (PixelBuffer),無需複製即可傳遞給 VideoToolbox 等元件

版本資訊

  • crate 名稱: shiguredo_video_device
  • 版本: 2026.4.0
  • Rust 版次: 2024
  • 最低 Rust 版本: 1.95
  • 最低 macOS 版本: 14.0 (因參考 AVCaptureDeviceTypeExternal,故不支援 macOS 13 及更早版本)
  • 授權條款: Apache-2.0
  • 儲存庫: https://github.com/shiguredo/video-device-rs

Error / PixelFormat 並未加上 #[non_exhaustive]。因此可以在 match 中窮舉所有變體。

導入

[dependencies]
shiguredo_video_device = "2026.4"

預設情況下,macOS 使用 AVFoundation、Linux 使用 V4L2、Windows 使用 Media Foundation 作為預設後端。 由於非建構目標 OS 的 feature 會被忽略,因此無需針對不同作業系統分別管理相依性。

feature

feature目標 OS說明
avfmacOS啟用 AVFoundation 後端
v4l2Linux啟用 V4L2 後端
pipewireLinux啟用 PipeWire 後端(動態連結 libpipewire-0.3)
mfWindows啟用 Media Foundation 後端(相依於 windows crate)
mjpegLinux啟用 V4L2 的 MJPEG 直通擷取(需包含 v4l2)
default-avfmacOS將預設後端設為 AVFoundation(包含 avf)
default-v4l2Linux將預設後端設為 V4L2(包含 v4l2)
default-pipewireLinux將預設後端設為 PipeWire(包含 pipewire)
default-mfWindows將預設後端設為 Media Foundation(包含 mf)

預設 feature 為 default-avf / default-v4l2 / default-mf。

  • 在同一個作業系統中啟用兩個以上的 default-* 會導致建構腳本 panic,無法建構
  • 在 Linux 上若要將預設後端設為 PipeWire,則需要設定 default-features = false。此時 macOS / Windows 的預設也會被移除,因此跨平台應用程式需要重新指定 default-avf / default-mf
  • 在沒有 default-* 的作業系統上,VideoDeviceList::enumerate() / VideoCapture::new() 將不存在。請改用 enumerate_avf() / new_v4l2() 等明確的 API
  • 在未啟用任何後端的作業系統上,VideoCapture / VideoDevice / VideoDeviceList 將不會公開(僅保留 Error / PixelFormat 等型別)
  • 在 Linux 上可以同時啟用 v4l2 與 pipewire
  • crate 內部的 enable_* cfg 僅由建構腳本為此 crate 設定,使用端無法參考。使用端應使用 #[cfg(target_os = "linux")] 搭配自身的 feature 進行分支

若要在 Linux 上將預設後端設為 PipeWire,同時保留 macOS / Windows 的預設設定:

[dependencies.shiguredo_video_device]
version = "2026.4"
default-features = false
features = ["default-avf", "default-mf", "default-pipewire"]

若要在預設為 V4L2 的同時,也啟用 PipeWire 與 MJPEG:

[dependencies]
shiguredo_video_device = { version = "2026.4", features = ["pipewire", "mjpeg"] }

建構需求

OS需求
macOSXcode Command Line Tools(需包含 bindgen 所需的 libclang)
Linux (V4L2)libclang(Ubuntu 上為 libclang-dev)
Linux (PipeWire)libclang 與 libpipewire-0.3-dev、pkg-config
Windows無需額外的相依套件

核心 API

公開型別

型別說明主要 API
VideoDeviceList裝置列舉結果enumerate(), enumerate_avf(), enumerate_v4l2(), enumerate_pipewire(), enumerate_mf(), as_slice(), len(), is_empty(), &VideoDeviceList: IntoIterator
VideoDevice單一裝置name() -> Result<String>, unique_id() -> Result<String>, formats() -> Vec<VideoFormat>, format_count() -> usize
VideoFormat裝置回報的格式width: i32, height: i32, min_fps: f32, max_fps: f32, pixel_format: PixelFormat
VideoCapture擷取會話new(), new_avf(), new_v4l2(), new_pipewire(), new_mf(), start(), stop(), config()
VideoCaptureConfig擷取設定(已實作 Default)device_id: Option<String>, width: i32, height: i32, fps: i32, pixel_format: Option<PixelFormat>
VideoFrame<'a>傳遞給回呼的借用幀data, uv_data, width, height, stride, stride_uv, pixel_format, timestamp_us, pixel_buffer, to_owned()
VideoFrameOwned擁有所有權的幀與 VideoFrame 相同的欄位(以 Vec<u8> 儲存), as_frame()
PixelFormat像素格式Nv12, Yuy2, I420, Mjpeg, Unknown(u32), name(), to_raw(), Display
PixelBuffermacOS 的 CVPixelBuffer 參考as_ptr() -> *mut c_void (Clone 時 CFRetain,Drop 時 CFRelease)
Error / Result<T>錯誤型別與結果型別請參考「錯誤型別」一節

enumerate() / new() 會使用預設後端 (default-*)。enumerate_*() / new_*() 僅在目標作業系統相符且對應的 feature 已啟用時才存在。

VideoDeviceList / VideoDevice

  • VideoDevice 只能以借用 (&VideoDevice) 的形式從 VideoDeviceList 中取得。若要在清單被拋棄後繼續使用資訊,請先複製 unique_id() 等值
  • unique_id() 的值可以直接傳遞給 VideoCaptureConfig::device_id(僅限相同後端)
  • name() / unique_id() 在 FFI 後端 (macOS / Linux) 上,因為 C 端可能返回 NULL 或非 UTF-8 字串,因此會回傳 Result。在 Windows 上則不會失敗
  • format_count() 是 C 端回報的條目數,可能會大於 formats().len()(後者已排除無法取得的條目)
  • VideoFormat 的 min_fps / max_fps 是依賴後端的近似值(請參考「各後端行為差異」一節)

VideoCapture 的生命週期

  • new*() 僅建立會話,不會開始擷取(macOS 上若權限尚未決定,會在此觸發作業系統的對話框,並等待回應後才返回)
  • start() 是冪等的,若正在執行則回傳 Ok(())。允許在 stop() 之後再次呼叫 start()
  • stop() 是阻塞的,會等待擷取執行緒與回呼完成後才返回。若未在執行中則不做任何事
  • drop 時,若正在執行,則會先進行與 stop() 相同的阻塞停止,然後才銷毀會話
  • config() 會回傳傳遞給 new*() 的要求值。並非協商後的實際值,因此解析度、格式與 stride 應參考每個幀的欄位

VideoCaptureConfig

欄位Default 值說明
device_idNoneVideoDevice::unique_id() 的值。None 表示使用預設裝置
width640請求的寬度
height480請求的高度
fps30請求的幀率
pixel_formatNone請求的像素格式。None 表示由後端選擇
  • 在 Windows 上,若 width / height / fps 小於或等於 0,則會回傳 Error::InvalidCaptureConfig。其他後端則不會在 Rust 端進行驗證
  • pixel_format: Some(PixelFormat::Unknown(_)) 在所有後端都會回傳 Error::UnsupportedPixelFormat
  • pixel_format: Some(PixelFormat::Mjpeg) 僅能在 Linux V4L2 + mjpeg feature 下使用。否則會回傳 Error::UnsupportedPixelFormat(PixelFormat::Mjpeg)
  • 請求值僅是期望值,實際值由後端與裝置決定

PixelFormat

變體內容to_raw() (FourCC)
Nv12YUV 4:2:0 半平面模式(Y 平面 + UV 交錯平面)0x3231564E (NV12)
Yuy2YUV 4:2:2 封裝模式(Y0 U Y1 V)0x32595559 (YUY2)
I420YUV 4:2:0 平面模式(Y / U / V 三個平面)0x30323449 (I420)
MjpegMotion JPEG。解碼責任在於使用者0x47504A4D (MJPG)
Unknown(u32)未知的 FourCC儲存的值
  • name() 會回傳 "NV12" / "YUY2" / "I420" / "MJPEG" / "Unknown"。Display 在 Unknown 時會以 Unknown(0x????????) 格式包含該值
  • 目前的後端不會產生 Unknown。未知的格式會從 formats() 中被排除,其幀也不會傳遞給回呼

幀的記憶體佈局

格式datauv_datastridestride_uv
NV12Y 平面Some: UV 交錯平面(ceil(height / 2) 行)Y 的每行位元組數UV 的每行位元組數
I420Y 平面Some: U 平面後緊接 V 平面的連續區域(每個 stride_uv * ceil(height / 2) 位元組)Y 的每行位元組數U / V 各自的每行位元組數
YUY2封裝資料None每行位元組數0
MJPEGJPEG 酬載(長度為 data.len())None00
  • stride 可能包含行末的填補。若需要連續緩衝區,則應逐行重新填補(請參考「程式碼範例」中的 pack_frame)
  • NV12 / I420 / YUY2 的 data.len() 在 FFI 後端 (macOS / Linux) 上為 stride * height,在 Windows 上為 width * height(YUY2 則為 width * 2 * height)
  • timestamp_us 的單位是微秒,但起點因後端而異。僅用於計算幀間的相對時間,不應作為實際時鐘時間使用
  • pixel_buffer 在 macOS 上為 Some,在其他平台上則為 None

幀回呼的限制

回呼的型別為 F: Fn(VideoFrame<'_>) + Send + 'static。

  • VideoFrame 的切片僅在回呼執行期間有效。若要在回呼之後繼續使用,請使用 frame.to_owned() 複製成 VideoFrameOwned
  • 回呼在後端的擷取執行緒上執行。若執行重量級操作,可能會導致幀遺失。建議使用 sync_channel + try_send 等方式將幀傳遞給其他執行緒
  • 回呼並非 FnMut,因此更新狀態時需使用 Cell / RefCell / Mutex / 原子型別等內部可變性機制。由於不要求 Sync,因此回呼不會被並行呼叫
  • 回呼中不應發生 panic。在 macOS / Linux 上可能導致程序 abort。在 Windows 上,會導致擷取執行緒終止,之後的 start()(在 stop() 之後)會回傳 Error::CaptureFaulted(未經 stop() 的 start() 則會視為正在執行而回傳 Ok(()))。此函式庫不會使用 catch_unwind 進行攔截
  • 請勿在回呼內部呼叫 stop() 或 drop VideoCapture。這在 Linux (V4L2 / PipeWire) 上可能導致 use-after-free,在 macOS 上則可能導致死結。Windows 的 VideoCapture 為 !Send,因此在安全 Rust 中無法將其傳遞給回呼。請透過通道或旗標通知另一個執行緒來執行停止操作

執行緒安全性

型別SendSync備註
VideoDeviceList / VideoDeviceYesYes
VideoCapture (macOS / Linux)YesNo
VideoCapture (Windows)NoNo為持有 COM 初始化防護,應在建立的執行緒中使用,並在該執行緒上 drop
VideoFrameOwned / PixelBufferYesNoArc<VideoFrameOwned> 無法在執行緒間共用。若需分發給多個接收端,請 clone 或以 Mutex 包裹
ErrorYesYesCopy

程式碼範例

裝置列舉

use shiguredo_video_device::VideoDeviceList;

fn main() -> shiguredo_video_device::Result<()> {
    // 使用預設後端列舉裝置
    let devices = VideoDeviceList::enumerate()?;
    for device in &devices {
        // name() / unique_id() 在 FFI 後端可能失敗,因此回傳 Result
        println!("{} ({})", device.name()?, device.unique_id()?);
        for format in device.formats() {
            println!(
                "  {}x{} {:.0}-{:.0}fps {}",
                format.width, format.height, format.min_fps, format.max_fps, format.pixel_format
            );
        }
    }
    Ok(())
}

透過通道接收幀(建議模式)

use std::sync::mpsc::sync_channel;
use std::time::Duration;

use shiguredo_video_device::{VideoCapture, VideoCaptureConfig, VideoFrame, VideoFrameOwned};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 使用有限容量通道來施加背壓。無限制通道會在接收端較慢時導致記憶體持續增長
    let (tx, rx) = sync_channel::<VideoFrameOwned>(4);

    let config = VideoCaptureConfig {
        width: 1280,
        height: 720,
        fps: 30,
        ..VideoCaptureConfig::default()
    };

    let mut capture = VideoCapture::new(config, move |frame: VideoFrame<'_>| {
        // 切片僅在回呼中有效,因此先 to_owned() 複製再發送
        // 當接收端阻塞時,不阻塞並直接丟棄該幀
        let _ = tx.try_send(frame.to_owned());
    })?;

    capture.start()?;

    for _ in 0..30 {
        let frame = rx.recv_timeout(Duration::from_secs(5))?;
        // 實際解析度與格式請參考幀本身(config() 回傳的是請求值)
        println!(
            "{}x{} {} ts={}us",
            frame.width, frame.height, frame.pixel_format, frame.timestamp_us
        );
    }

    // stop() 會等待回呼完成後才返回。請勿在回呼內部呼叫
    capture.stop();
    Ok(())
}

指定裝置與格式

use shiguredo_video_device::{
    Error, PixelFormat, VideoCapture, VideoCaptureConfig, VideoDeviceList,
};

fn open_first_device() -> Result<VideoCapture, Box<dyn std::error::Error>> {
    let devices = VideoDeviceList::enumerate()?;
    let Some(device) = devices.as_slice().first() else {
        return Err("no video device found".into());
    };

    // 若裝置回報支援 1280x720 的 NV12 則請求該格式,否則交由後端決定
    let pixel_format = device
        .formats()
        .into_iter()
        .find(|f| f.width == 1280 && f.height == 720 && f.pixel_format == PixelFormat::Nv12)
        .map(|f| f.pixel_format);

    let config = VideoCaptureConfig {
        // unique_id() 的值可以直接傳遞給 device_id
        device_id: Some(device.unique_id()?),
        width: 1280,
        height: 720,
        fps: 30,
        pixel_format,
    };

    match VideoCapture::new(config, |frame| {
        let _ = frame.data.len();
    }) {
        Ok(capture) => Ok(capture),
        Err(Error::UnsupportedPixelFormat(format)) => {
            Err(format!("pixel format {format} is not supported").into())
        }
        Err(e) => Err(e.into()),
    }
}

移除 stride 以填補連續緩衝區

當編碼器等元件要求寬度恰好對齊的連續緩衝區時使用。

use shiguredo_video_device::{PixelFormat, VideoFrame};

/// 將帶有 stride 的平面中每行的有效位元組連接起來
fn pack_plane(src: &[u8], stride: usize, row_bytes: usize, rows: usize) -> Option<Vec<u8>> {
    if row_bytes > stride {
        return None;
    }
    // 尺寸源自裝置,不可信賴,因此不預先分配,僅依實際資料長度擴充
    let mut out = Vec::new();
    for row in 0..rows {
        let start = row.checked_mul(stride)?;
        out.extend_from_slice(src.get(start..start.checked_add(row_bytes)?)?);
    }
    Some(out)
}

/// 將幀填補成無 stride 的連續緩衝區。若尺寸不一致則回傳 None
fn pack_frame(frame: &VideoFrame<'_>) -> Option<Vec<u8>> {
    let w = usize::try_from(frame.width).ok()?;
    let h = usize::try_from(frame.height).ok()?;
    let stride = usize::try_from(frame.stride).ok()?;
    let stride_uv = usize::try_from(frame.stride_uv).ok()?;
    let chroma_w = w.div_ceil(2);
    let chroma_h = h.div_ceil(2);
    match frame.pixel_format {
        PixelFormat::Nv12 => {
            let mut out = pack_plane(frame.data, stride, w, h)?;
            // UV 是 U 與 V 交錯排列,因此一行有效位元組數為 chroma_w * 2
            out.extend(pack_plane(
                frame.uv_data?,
                stride_uv,
                chroma_w * 2,
                chroma_h,
            )?);
            Some(out)
        }
        PixelFormat::I420 => {
            let uv = frame.uv_data?;
            // uv_data 中 U 平面後緊接 V 平面
            let plane_size = stride_uv.checked_mul(chroma_h)?;
            let mut out = pack_plane(frame.data, stride, w, h)?;
            out.extend(pack_plane(
                uv.get(..plane_size)?,
                stride_uv,
                chroma_w,
                chroma_h,
            )?);
            out.extend(pack_plane(
                uv.get(plane_size..)?,
                stride_uv,
                chroma_w,
                chroma_h,
            )?);
            Some(out)
        }
        // YUY2 是 2 像素 4 位元組
        PixelFormat::Yuy2 => pack_plane(frame.data, stride, chroma_w * 4, h),
        // MJPEG 是無 stride 的 JPEG 酬載
        PixelFormat::Mjpeg => Some(frame.data.to_vec()),
        PixelFormat::Unknown(_) => None,
    }
}

明確指定非預設後端 (Linux PipeWire)

// 前提:已在相依性的 features 中啟用 pipewire
// 為避免在其他作業系統上產生未使用的匯入警告,請在 use 處加上 cfg
#[cfg(target_os = "linux")]
use shiguredo_video_device::VideoCapture;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    #[cfg(target_os = "linux")]
    {
        let mut capture = VideoCapture::new_pipewire(
            shiguredo_video_device::VideoCaptureConfig::default(),
            |frame| {
                println!("Got frame: {}x{}", frame.width, frame.height);
            },
        )?;
        capture.start()?;
        std::thread::sleep(std::time::Duration::from_secs(2));
        capture.stop();
    }
    Ok(())
}

錯誤型別

變體說明
Error::EnumerateFailed(String)列舉時後端發生錯誤
Error::OpenFailed(String)開啟裝置時後端發生錯誤
Error::StartFailed(String)開始擷取時後端發生錯誤
Error::StopFailed(String)停止擷取時後端發生錯誤
Error::InvalidCaptureConfig無效的 VideoCaptureConfig (Windows 上 width / height / fps <= 0)
Error::UnsupportedPixelFormat(PixelFormat)請求的像素格式不被該後端支援
Error::CaptureFaulted擷取執行緒因回呼 panic 而終止(僅 Windows)
Error::Internal(String)內部不一致或其他未歸類的錯誤

各後端行為差異

此節整理了筆者觀察到的差異。並非所有行為都經過嚴謹測試,可能因 OS 版本、驅動程式或硬體而異。

裝置列舉 (formats() / format_count())

  • macOS (AVFoundation): 透過 AVCaptureDevice 列舉。formats() 會回傳完整的格式列表。format_count() 與 formats().len() 應相等
  • Linux (V4L2): 透過 VIDIOC_ENUM_FMT 與 VIDIOC_ENUM_FRAMESIZES 以及 VIDIOC_ENUM_FRAMEINTERVALS 進行列舉。部分裝置或驅動程式可能報告錯誤的解析度或格式。format_count() 可能大於 formats().len(),因為部分條目可能因 ioctl 失敗而被排除
  • Linux (PipeWire): 使用 PipeWire 的 registry 與參數列舉。行爲與 V4L2 相似,但依賴 PipeWire 的圖形管道
  • Windows (Media Foundation): 透過 IMFMediaSource 列舉。行為相對穩定

解析度與幀率協商

  • macOS (AVFoundation): 透過 AVCaptureDeviceFormat 進行協商。min_fps / max_fps 是從格式的 videoSupportedFrameRateRanges 中取得
  • Linux (V4L2): 透過 VIDIOC_S_FMT 與 VIDIOC_S_PARM 進行協商。實際獲得的解析度與幀率可能與請求的不同
  • Linux (PipeWire): 協商透過 PipeWire 的參數協商機制進行
  • Windows (Media Foundation): 透過 IMFMediaType 進行協商

PixelBuffer (macOS 專用)

  • 僅在 macOS 上,VideoFrame 的 pixel_buffer 欄位為 Some,包含對 CVPixelBuffer 的參考
  • 這使得可以無需複製地將資料傳遞給 VideoToolbox 等框架進行編碼或處理
  • 在其他平台上,pixel_buffer 始終為 None

MJPEG 支援 (Linux V4L2)

  • 僅在 Linux V4L2 且啟用 mjpeg feature 時支援 PixelFormat::Mjpeg
  • 在此模式下,VideoFrame::data 包含原始的 JPEG 酬載,長度為 data.len(),stride 與 stride_uv 為 0
  • 解碼 JPEG 的責任在於使用者

常見問題

Q: 如何在回呼中更新外部狀態?

A: 由於回呼是 Fn 而非 FnMut,您需要使用內部可變性,例如 Arc<Mutex<State>>、Cell(適用於 Copy 型別)或原子型別。由於回呼是 Send,因此 Arc<Mutex<State>> 是安全的。

Q: 如何優雅地停止擷取?

A: 在回呼外部(例如另一個執行緒或主執行緒)呼叫 capture.stop()。請勿在回呼內部呼叫 stop(),這可能導致死結或 use-after-free。使用 Arc<AtomicBool> 作為標誌,在回呼中檢查該標誌,當需要停止時從外部將標誌設為 true,然後在另一個執行緒上呼叫 stop()。

Q: 為什麼 VideoCapture 在 Windows 上是 !Send?

A: 因為它在內部持有 COM 初始化防護 (CoInitializeEx / CoUninitialize)。COM 要求物件在初始化的 Apartment 中使用與釋放。因此,請確保在同一個執行緒上建立、使用 (start / stop) 並 drop VideoCapture。

Q: 不同後端之間 unique_id() 的格式是否一致?

A: 不一致。unique_id() 的格式因後端而異(例如 macOS 可能會返回 0x1a2b3c4d5e6f7g8h 之類的唯一識別碼,而 Linux 可能返回 /dev/video0)。請勿假設其格式的一致性或可移植性。

Q: 支援哪些 Rust 版本?

A: 最低 Rust 版本為 1.95。

Q: 如何貢獻?

A: 請參考儲存庫中的 CONTRIBUTING.md。

授權條款

Apache-2.0

相關技能