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 直通(使用
mjpegfeature) - 低相依性: Rust 的相依 crate 僅有 Windows (
mffeature) 使用的windowscrate - 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 | 說明 |
|---|---|---|
avf | macOS | 啟用 AVFoundation 後端 |
v4l2 | Linux | 啟用 V4L2 後端 |
pipewire | Linux | 啟用 PipeWire 後端(動態連結 libpipewire-0.3) |
mf | Windows | 啟用 Media Foundation 後端(相依於 windows crate) |
mjpeg | Linux | 啟用 V4L2 的 MJPEG 直通擷取(需包含 v4l2) |
default-avf | macOS | 將預設後端設為 AVFoundation(包含 avf) |
default-v4l2 | Linux | 將預設後端設為 V4L2(包含 v4l2) |
default-pipewire | Linux | 將預設後端設為 PipeWire(包含 pipewire) |
default-mf | Windows | 將預設後端設為 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 | 需求 |
|---|---|
| macOS | Xcode 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 |
PixelBuffer | macOS 的 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_id | None | VideoDevice::unique_id() 的值。None 表示使用預設裝置 |
width | 640 | 請求的寬度 |
height | 480 | 請求的高度 |
fps | 30 | 請求的幀率 |
pixel_format | None | 請求的像素格式。None 表示由後端選擇 |
- 在 Windows 上,若
width/height/fps小於或等於 0,則會回傳Error::InvalidCaptureConfig。其他後端則不會在 Rust 端進行驗證 pixel_format: Some(PixelFormat::Unknown(_))在所有後端都會回傳Error::UnsupportedPixelFormatpixel_format: Some(PixelFormat::Mjpeg)僅能在 Linux V4L2 +mjpegfeature 下使用。否則會回傳Error::UnsupportedPixelFormat(PixelFormat::Mjpeg)- 請求值僅是期望值,實際值由後端與裝置決定
PixelFormat
| 變體 | 內容 | to_raw() (FourCC) |
|---|---|---|
Nv12 | YUV 4:2:0 半平面模式(Y 平面 + UV 交錯平面) | 0x3231564E (NV12) |
Yuy2 | YUV 4:2:2 封裝模式(Y0 U Y1 V) | 0x32595559 (YUY2) |
I420 | YUV 4:2:0 平面模式(Y / U / V 三個平面) | 0x30323449 (I420) |
Mjpeg | Motion JPEG。解碼責任在於使用者 | 0x47504A4D (MJPG) |
Unknown(u32) | 未知的 FourCC | 儲存的值 |
name()會回傳"NV12"/"YUY2"/"I420"/"MJPEG"/"Unknown"。Display在Unknown時會以Unknown(0x????????)格式包含該值- 目前的後端不會產生
Unknown。未知的格式會從formats()中被排除,其幀也不會傳遞給回呼
幀的記憶體佈局
| 格式 | data | uv_data | stride | stride_uv |
|---|---|---|---|---|
| NV12 | Y 平面 | Some: UV 交錯平面(ceil(height / 2) 行) | Y 的每行位元組數 | UV 的每行位元組數 |
| I420 | Y 平面 | Some: U 平面後緊接 V 平面的連續區域(每個 stride_uv * ceil(height / 2) 位元組) | Y 的每行位元組數 | U / V 各自的每行位元組數 |
| YUY2 | 封裝資料 | None | 每行位元組數 | 0 |
| MJPEG | JPEG 酬載(長度為 data.len()) | None | 0 | 0 |
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()或 dropVideoCapture。這在 Linux (V4L2 / PipeWire) 上可能導致 use-after-free,在 macOS 上則可能導致死結。Windows 的VideoCapture為!Send,因此在安全 Rust 中無法將其傳遞給回呼。請透過通道或旗標通知另一個執行緒來執行停止操作
執行緒安全性
| 型別 | Send | Sync | 備註 |
|---|---|---|---|
VideoDeviceList / VideoDevice | Yes | Yes | |
VideoCapture (macOS / Linux) | Yes | No | |
VideoCapture (Windows) | No | No | 為持有 COM 初始化防護,應在建立的執行緒中使用,並在該執行緒上 drop |
VideoFrameOwned / PixelBuffer | Yes | No | Arc<VideoFrameOwned> 無法在執行緒間共用。若需分發給多個接收端,請 clone 或以 Mutex 包裹 |
Error | Yes | Yes | Copy |
程式碼範例
裝置列舉
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 且啟用
mjpegfeature 時支援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