shiguredo_video_device
macOS / Linux / Windows のカメラ列挙とフレームキャプチャを、プラットフォーム共通の Rust API で提供するライブラリ。
特徴
- クロスプラットフォーム: macOS (AVFoundation)、Linux (V4L2 / PipeWire)、Windows (Media Foundation) を同じ API で扱う
- feature によるバックエンド選択: 有効化するバックエンドと既定バックエンドを feature で選ぶ
- コールバック駆動: キャプチャ中はバックエンドのスレッド上でフレームごとにコールバックが呼ばれる
- ピクセルフォーマット: NV12 / YUY2 / I420、Linux (V4L2) では MJPEG パススルー (
mjpegfeature) - 少ない依存: Rust の依存クレートは Windows (
mffeature) のwindowsクレートのみ - CVPixelBuffer 参照 (macOS): フレームに CVPixelBuffer への参照 (
PixelBuffer) が付き、コピーせずに VideoToolbox などへ渡せる
バージョン情報
- crate 名:
shiguredo_video_device - バージョン: 2026.4.0
- Rust Edition: 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 は無視されるため、OS ごとに依存を分ける必要はない。
feature
| feature | 対象 OS | 説明 |
|---|---|---|
avf | macOS | AVFoundation バックエンドを有効化する |
v4l2 | Linux | V4L2 バックエンドを有効化する |
pipewire | Linux | PipeWire バックエンドを有効化する (libpipewire-0.3 に動的リンクする) |
mf | Windows | Media Foundation バックエンドを有効化する (windows クレートに依存する) |
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。
- 同一 OS で
default-*を 2 つ以上有効にすると、ビルドスクリプトが panic してビルドできない - Linux で既定を PipeWire にするには
default-features = falseが必要。このとき macOS / Windows の既定も外れるため、クロスプラットフォームのアプリではdefault-avf/default-mfを指定し直す default-*が無い OS ではVideoDeviceList::enumerate()/VideoCapture::new()が存在しない。enumerate_avf()/new_v4l2()などの明示 API を使う- バックエンドが 1 つも有効でない OS では
VideoCapture/VideoDevice/VideoDeviceListが公開されない (Error/PixelFormatなどの型だけが残る) - Linux では
v4l2とpipewireを同時に有効化できる - クレート内部の
enable_*cfg はビルドスクリプトがこのクレートにだけ設定するもので、利用側からは参照できない。利用側は#[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 | 1 台のデバイス | 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_*() は、ビルド対象 OS が一致し、対応する feature が有効なときだけ存在する。
VideoDeviceList / VideoDevice
VideoDeviceはVideoDeviceListからの借用 (&VideoDevice) でしか取り出せない。リストを捨てた後も情報を使う場合は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 は権限が未決定ならここで OS のダイアログを出し、応答まで戻らない)start()は冪等で、実行中ならOk(())を返す。stop()後の再start()も許容するstop()はブロッキングで、キャプチャスレッドとコールバックの完了を待ってから戻る。実行中でなければ何もしないdropすると、実行中ならstop()と同じブロッキング停止を行ってからセッションを破棄するconfig()はnew*()に渡した要求値を返す。ネゴシエーション後の実際の値ではないため、解像度・フォーマット・ストライドは各フレームのフィールドを参照する
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::UnsupportedPixelFormatを返すpixel_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 の 3 プレーン) | 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 の 1 行のバイト数 | UV の 1 行のバイト数 |
| I420 | Y プレーン | Some: U プレーンの直後に V プレーンが続く連結領域 (各 stride_uv * ceil(height / 2) バイト) | Y の 1 行のバイト数 | U / V それぞれの 1 行のバイト数 |
| YUY2 | パックデータ | None | 1 行のバイト数 | 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 ではキャプチャスレッドが終了し、
stop()の後のstart()がError::CaptureFaultedを返す (stop()を挟まないstart()は実行中とみなしてOk(())を返す)。ライブラリはcatch_unwindで吸収しない - コールバック内から
stop()を呼んだりVideoCaptureを drop したりしてはならない。Linux (V4L2 / PipeWire) では use-after-free、macOS ではデッドロックになりうる。Windows のVideoCaptureは!Sendのため、safe Rust ではコールバックに渡せない。停止はチャネルやフラグで別スレッドに通知し、そのスレッドでstop()を呼ぶ
スレッド安全性
| 型 | 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()?),