shiguredo_video_device
Eine Bibliothek zur Kameraaufzählung und Frame-Aufnahme unter macOS / Linux / Windows mit einer einheitlichen Rust-API.
Merkmale
- Plattformübergreifend: macOS (AVFoundation), Linux (V4L2 / PipeWire), Windows (Media Foundation) mit derselben API
- Backend-Auswahl über Features: Das aktive Backend wird über Cargo-Features gesteuert
- Callback-gesteuert: Während der Aufnahme wird für jeden Frame ein Callback im Thread des Backends aufgerufen
- Pixelformate: NV12 / YUY2 / I420, unter Linux (V4L2) auch MJPEG-Passthrough (
mjpegFeature) - Wenige Abhängigkeiten: Auf Rust-Seite wird nur unter Windows (
mfFeature) diewindows-Crate benötigt - CVPixelBuffer-Referenz (macOS): Der Frame enthält eine Referenz auf den CVPixelBuffer (
PixelBuffer), die ohne Kopie an VideoToolbox etc. übergeben werden kann
Versionsinformationen
- Crate-Name:
shiguredo_video_device - Version: 2026.4.0
- Rust Edition: 2024
- Minimale Rust-Version: 1.95
- Minimale macOS-Version: 14.0 (verwendet
AVCaptureDeviceTypeExternal, daher nicht auf macOS 13 oder älter lauffähig) - Lizenz: Apache-2.0
- Repository: https://github.com/shiguredo/video-device-rs
Error / PixelFormat sind nicht #[non_exhaustive]. Alle Varianten können in einem match aufgezählt werden.
Einführung
[dependencies]
shiguredo_video_device = "2026.4"
Standardmäßig wird unter macOS AVFoundation, unter Linux V4L2 und unter Windows Media Foundation als Backend verwendet. Features für ein anderes Betriebssystem werden ignoriert; eine plattformspezifische Trennung der Abhängigkeiten ist nicht nötig.
Features
| Feature | Ziel-OS | Beschreibung |
|---|---|---|
avf | macOS | Aktiviert das AVFoundation-Backend |
v4l2 | Linux | Aktiviert das V4L2-Backend |
pipewire | Linux | Aktiviert das PipeWire-Backend (dynamische Verlinkung mit libpipewire-0.3) |
mf | Windows | Aktiviert das Media Foundation-Backend (Abhängigkeit von der windows-Crate) |
mjpeg | Linux | Aktiviert MJPEG-Passthrough unter V4L2 (beinhaltet v4l2) |
default-avf | macOS | Setzt AVFoundation als Standard-Backend (beinhaltet avf) |
default-v4l2 | Linux | Setzt V4L2 als Standard-Backend (beinhaltet v4l2) |
default-pipewire | Linux | Setzt PipeWire als Standard-Backend (beinhaltet pipewire) |
default-mf | Windows | Setzt Media Foundation als Standard-Backend (beinhaltet mf) |
Standard-Features sind default-avf / default-v4l2 / default-mf.
- Wenn auf demselben OS zwei oder mehr
default-*-Features aktiviert sind, kann das Build-Skript nicht kompilieren (panic). - Um unter Linux PipeWire als Standard zu setzen, muss
default-features = falsegesetzt werden. Dann entfallen auch die macOS-/Windows-Standards; in plattformübergreifenden Anwendungen müssendefault-avf/default-mfwieder explizit angegeben werden. - Auf einem OS ohne
default-*-Feature existierenVideoDeviceList::enumerate()/VideoCapture::new()nicht. Es müssen dann die expliziten APIs (enumerate_avf()/new_v4l2()usw.) verwendet werden. - Wenn für ein OS kein Backend aktiviert ist, werden
VideoCapture/VideoDevice/VideoDeviceListnicht exportiert (nurError/PixelFormatusw. bleiben sichtbar). - Unter Linux können
v4l2undpipewiregleichzeitig aktiviert werden. - Die internen
enable_*-cfg-Optionen werden vom Build-Skript nur für diese Crate gesetzt und sind von außen nicht referenzierbar. Externer Code sollte mit#[cfg(target_os = "linux")]und eigenen Features verzweigen.
Wenn unter Linux PipeWire als Standard verwendet und macOS/Windows auf den Standardeinstellungen belassen werden soll:
[dependencies.shiguredo_video_device]
version = "2026.4"
default-features = false
features = ["default-avf", "default-mf", "default-pipewire"]
Wenn V4L2 der Standard bleiben, aber PipeWire und MJPEG zusätzlich nutzbar sein sollen:
[dependencies]
shiguredo_video_device = { version = "2026.4", features = ["pipewire", "mjpeg"] }
Build-Voraussetzungen
| OS | Anforderung |
|---|---|
| macOS | Xcode Command Line Tools (enthält libclang für bindgen) |
| Linux (V4L2) | libclang (Ubuntu: libclang-dev) |
| Linux (PipeWire) | libclang, libpipewire-0.3-dev, pkg-config |
| Windows | Keine zusätzlichen Pakete erforderlich |
Kern-API
Exportierte Typen
| Typ | Beschreibung | Haupt-API |
|---|---|---|
VideoDeviceList | Ergebnis der Geräteaufzählung | enumerate(), enumerate_avf(), enumerate_v4l2(), enumerate_pipewire(), enumerate_mf(), as_slice(), len(), is_empty(), &VideoDeviceList: IntoIterator |
VideoDevice | Ein einzelnes Gerät | name() -> Result<String>, unique_id() -> Result<String>, formats() -> Vec<VideoFormat>, format_count() -> usize |
VideoFormat | Ein vom Gerät gemeldetes Format | width: i32, height: i32, min_fps: f32, max_fps: f32, pixel_format: PixelFormat |
VideoCapture | Eine Capture-Session | new(), new_avf(), new_v4l2(), new_pipewire(), new_mf(), start(), stop(), config() |
VideoCaptureConfig | Capture-Konfiguration (implementiert Default) | device_id: Option<String>, width: i32, height: i32, fps: i32, pixel_format: Option<PixelFormat> |
VideoFrame<'a> | Ein geliehener Frame im Callback | data, uv_data, width, height, stride, stride_uv, pixel_format, timestamp_us, pixel_buffer, to_owned() |
VideoFrameOwned | Ein eigener Frame | Gleiche Felder wie VideoFrame (gespeichert als Vec<u8>), as_frame() |
PixelFormat | Pixelformat | Nv12, Yuy2, I420, Mjpeg, Unknown(u32), name(), to_raw(), Display |
PixelBuffer | Referenz auf einen macOS-CVPixelBuffer | as_ptr() -> *mut c_void (Clone erhöht den Referenzzähler via CFRetain, Drop gibt ihn frei via CFRelease) |
Error / Result<T> | Fehler- und Ergebnistypen | Siehe Abschnitt „Fehlertypen“ |
enumerate() / new() verwenden das Standard-Backend (default-*). enumerate_*() / new_*() existieren nur, wenn das Ziel-OS und das entsprechende Feature passen.
VideoDeviceList / VideoDevice
- Ein
VideoDevicekann nur als Ausleihe (&VideoDevice) aus einerVideoDeviceListentnommen werden. Sollen die Informationen nach dem Verwerfen der Liste weiterverwendet werden, müssen die Werte (z. B.unique_id()) vorher kopiert werden. - Der Wert von
unique_id()kann direkt alsVideoCaptureConfig::device_idverwendet werden (allerdings nur innerhalb desselben Backends). name()/unique_id()gebenResultzurück, da die C-FFI-Backends (macOS/Linux) NULL oder nicht-UTF-8-konforme Werte liefern können. Unter Windows schlagen sie nicht fehl.format_count()ist die Anzahl der vom C-Backend gemeldeten Einträge und kann größer sein alsformats().len(), da nicht alle Einträge erfolgreich abgerufen werden können.min_fps/max_fpsinVideoFormatsind backend-abhängige Schätzwerte (siehe Abschnitt „Backend-Unterschiede“).
Lebenszyklus von VideoCapture
new*()erzeugt nur eine Session, die Aufnahme startet noch nicht (unter macOS wird ggf. der Berechtigungsdialog angezeigt; die Funktion kehrt erst zurück, wenn der Dialog beantwortet wurde).start()ist idempotent: Wenn die Aufnahme bereits läuft, wirdOk(())zurückgegeben. Ein erneuterstart()-Aufruf nachstop()ist erlaubt.stop()blockiert, bis der Capture-Thread und alle Callbacks beendet sind. Wenn die Aufnahme nicht läuft, passiert nichts.- Beim
dropwird eine laufende Aufnahme blockierend gestoppt (wiestop()), bevor die Session zerstört wird. config()gibt die beinew*()übergebenen Wunschwerte zurück – nicht die tatsächlich ausgehandelten Werte. Für die tatsächliche Auflösung, das Format und den Stride müssen die Felder des jeweiligen Frames verwendet werden.
VideoCaptureConfig
| Feld | Default-Wert | Beschreibung |
|---|---|---|
device_id | None | Wert von VideoDevice::unique_id(). None wählt das Standardgerät. |
width | 640 | Gewünschte Breite |
height | 480 | Gewünschte Höhe |
fps | 30 | Gewünschte Bildrate |
pixel_format | None | Gewünschtes Pixelformat. None überlässt die Wahl dem Backend. |
- Unter Windows wird
Error::InvalidCaptureConfigzurückgegeben, wennwidth/height/fps<= 0 sind. Andere Backends prüfen dies nicht auf Rust-Seite. pixel_format: Some(PixelFormat::Unknown(_))führt bei allen Backends zuError::UnsupportedPixelFormat.pixel_format: Some(PixelFormat::Mjpeg)ist nur unter Linux mit V4L2 und demmjpeg-Feature verfügbar. Ansonsten wirdError::UnsupportedPixelFormat(PixelFormat::Mjpeg)zurückgegeben.- Die angeforderten Werte sind nur Wünsche; die tatsächlichen Werte bestimmen Backend und Gerät.
PixelFormat
| Variante | Inhalt | to_raw() (FourCC) |
|---|---|---|
Nv12 | YUV 4:2:0 Semi-Planar (Y-Ebene + UV-interleaved-Ebene) | 0x3231564E (NV12) |
Yuy2 | YUV 4:2:2 Packed (Y0 U Y1 V) | 0x32595559 (YUY2) |
I420 | YUV 4:2:0 Planar (3 Ebenen: Y / U / V) | 0x30323449 (I420) |
Mjpeg | Motion JPEG. Dekodierung obliegt dem Anwender. | 0x47504A4D (MJPG) |
Unknown(u32) | Unbekannter FourCC | Der gespeicherte Wert |
name()gibt"NV12"/"YUY2"/"I420"/"MJPEG"/"Unknown"zurück. DieDisplay-Implementierung gibt beiUnknownden Wert im FormatUnknown(0x????????)aus.- Aktuelle Backends erzeugen nie
Unknown. Unbekannte Formate werden vonformats()ausgeschlossen und die entsprechenden Frames nicht an den Callback übergeben.
Speicherlayout von Frames
| Format | data | uv_data | stride | stride_uv |
|---|---|---|---|---|
| NV12 | Y-Ebene | Some: UV-interleaved-Ebene (ceil(height / 2) Zeilen) | Bytezahl einer Y-Zeile | Bytezahl einer UV-Zeile |
| I420 | Y-Ebene | Some: UV-interleaved-Ebene – direkt nach der U-Ebene folgt die V-Ebene (jeweils stride_uv * ceil(height / 2) Bytes) | Bytezahl einer Y-Zeile | Bytezahl einer U- bzw. V-Zeile |
| YUY2 | Gepackte Daten | None | Bytezahl einer Zeile | 0 |
| MJPEG | JPEG-Nutzdaten (Länge in data.len()) | None | 0 | 0 |
stridekann Padding am Zeilenende enthalten. Wenn ein durchgehender Puffer ohne Padding benötigt wird, müssen die Daten zeilenweise umkopiert werden (siehepack_frameim Codebeispiel).data.len()für NV12 / I420 / YUY2 beträgt bei den FFI-Backends (macOS/Linux)stride * height, unter Windowswidth * height(YUY2:width * 2 * height).timestamp_usist in Mikrosekunden, der Nullpunkt variiert jedoch je nach Backend. Der Wert sollte nur für relative Zeitabstände zwischen Frames verwendet werden, nicht als Echtzeituhr.pixel_bufferist auf macOSSome, auf anderen PlattformenNone.
Einschränkungen des Frame-Callbacks
Der Callback-Typ ist F: Fn(VideoFrame<'_>) + Send + 'static.
- Die Slice-Daten von
VideoFramesind nur innerhalb des Callbacks gültig. Sollen sie später verwendet werden, muss mitframe.to_owned()eine Kopie (VideoFrameOwned) erstellt werden. - Der Callback wird im Capture-Thread des Backends ausgeführt. Aufwändige Verarbeitung führt zu verlorenen Frames; daher sollte die Arbeit über z. B.
sync_channel+try_sendan einen anderen Thread delegiert werden. - Da der Callback
Fn(nichtFnMut) ist, muss für Zustandsänderungen innere Veränderbarkeit verwendet werden (Cell/RefCell/Mutex/ atomare Typen). Der Callback wird nicht parallel aufgerufen (Syncist nicht erforderlich). - Der Callback darf nicht panic. Unter macOS/Linux kann dies zum Absturz des Prozesses führen. Unter Windows wird der Capture-Thread beendet und nachfolgende
start()-Aufrufe (nach einemstop()) gebenError::CaptureFaultedzurück. Die Bibliothek fängt Panics nicht mitcatch_unwindab. - Innerhalb des Callbacks darf weder
stop()aufgerufen nochVideoCapturegedroppt werden. Unter Linux (V4L2 / PipeWire) kann dies zu Use-after-Free, unter macOS zu Deadlocks führen. Unter Windows istVideoCaptureohnehin!Sendund kann in safe Rust nicht in den Callback gegeben werden. Das Stoppen sollte über einen Kanal oder ein Flag an einen anderen Thread signalisiert werden, der dannstop()aufruft.
Thread-Sicherheit
| Typ | Send | Sync | Anmerkungen |
|---|---|---|---|
VideoDeviceList / VideoDevice | Yes | Yes | |
VideoCapture (macOS/Linux) | Yes | No | |
VideoCapture (Windows) | No | No | Enthält einen COM-Initialisierungs-Guard; muss im Thread erstellt und wieder dort gedroppt werden. |
VideoFrameOwned / PixelBuffer | Yes | No | Arc<VideoFrameOwned> kann nicht zwischen Threads geteilt werden. Für mehrere Empfänger entweder klonen oder in einen Mutex einpacken. |
Error | Yes | Yes | Copy |
Codebeispiele
Geräteaufzählung
use shiguredo_video_device::VideoDeviceList;
fn main() -> shiguredo_video_device::Result<()> {
// Geräte mit dem Standard-Backend aufzählen
let devices = VideoDeviceList::enumerate()?;
for device in &devices {
// name() / unique_id() können bei FFI-Backends fehlschlagen und geben Result zurück
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(())
}
Frames über einen Kanal empfangen (empfohlenes Muster)
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>> {
// Begrenzter Kanal zur Erzeugung von Gegendruck. Unbegrenzte Kanäle können bei langsamen Empfängern unendlich viel Speicher verbrauchen.
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<'_>| {
// Die Slice-Daten sind nur innerhalb des Callbacks gültig, daher mit to_owned() kopieren.
// Wenn der Empfänger blockiert, wird der Frame verworfen (kein Blockieren).
let _ = tx.try_send(frame.to_owned());
})?;
capture.start()?;
for _ in 0..30 {
let frame = rx.recv_timeout(Duration::from_secs(5))?;
// Tatsächliche Auflösung und Format stehen im Frame (config() gibt nur die Wunschwerte zurück).
println!(
"{}x{} {} ts={}us",
frame.width, frame.height, frame.pixel_format, frame.timestamp_us
);
}
// stop() blockiert, bis die Callbacks abgeschlossen sind. Darf nicht innerhalb eines Callbacks aufgerufen werden.
capture.stop();
Ok(())
}
Gerät und Format explizit angeben
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());
};
// Wenn das Gerät 1280x720 NV12 meldet, wird dies angefordert; ansonsten überlässt man die Wahl dem Backend.
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 {
// Der Wert von unique_id() kann direkt als device_id verwendet werden.
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!("Pixelformat {format} wird nicht unterstützt").into())
}
Err(e) => Err(e.into()),
}
}
Stride entfernen und in einen kontinuierlichen Puffer packen
Dies wird z. B. benötigt, wenn ein Encoder einen durchgehenden Puffer ohne Padding erwartet.
use shiguredo_video_device::{PixelFormat, VideoFrame};
/// Verkettet nur die Nutzdaten jeder Zeile aus einer Ebene mit Stride.
fn pack_plane(src: &[u8], stride: usize, row_bytes: usize, rows: usize) -> Option<Vec<u8>> {
if row_bytes > stride {
return None;
}
// Die Abmessungen stammen vom Gerät und sind nicht vertrauenswürdig; daher nicht vorab allozieren, sondern nur so viel wie nötig anhängen.
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)
}
/// Packt einen Frame ohne Stride in einen kontinuierlichen Puffer. Gibt None zurück, wenn die Abmessungen inkonsistent sind.
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 und V wechseln sich ab, daher Nutzdaten pro Zeile = 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: direkt nach der U-Ebene beginnt die V-Ebene
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: 4 Bytes pro 2 Pixel
PixelFormat::Yuy2 => pack_plane(frame.data, stride, chroma_w * 4, h),
// MJPEG hat keinen Stride
PixelFormat::Mjpeg => Some(frame.data.to_vec()),
PixelFormat::Unknown(_) => None,
}
}
Explizite Backend-Wahl (Linux PipeWire)
// Voraussetzung: Das Feature "pipewire" ist in den Abhängigkeiten aktiviert.
// Um "unused import" auf anderen Plattformen zu vermeiden, wird cfg am use angebracht.
#[cfg(target_os = "linux")]
use shiguredo_video_device::{VideoCapture, VideoCaptureConfig};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// [...]
# Ok(())
}
Fehlertypen
| Fehler | Beschreibung |
|---|---|
Error::NoBackend | Kein Backend ist für die aktuelle Plattform aktiviert. |
Error::UnsupportedPixelFormat(PixelFormat) | Das angeforderte Pixelformat wird nicht unterstützt. |
Error::InvalidCaptureConfig | Ungültige Konfiguration (z. B. negative Maße unter Windows). |
Error::NoDevice | Kein Gerät gefunden. |
Error::CaptureFailed(String) | Allgemeiner Fehler bei der Aufnahme. |
Error::CaptureFaulted | Die Aufnahme ist aufgrund eines Fehlers abgestürzt. |
Backend-Unterschiede
- macOS (AVFoundation): Erfordert macOS 14.0+.
min_fpsundmax_fpssind Schätzwerte. - Linux V4L2:
min_fpsundmax_fpswerden so genau wie möglich aus den Treiberdaten ermittelt. - Linux PipeWire: Dynamische Verlinkung mit
libpipewire-0.3. Die fps-Werte sind Schätzwerte. - Windows (Media Foundation):
VideoCaptureist!Send.min_fpsundmax_fpssind Schätzwerte. - MJPEG (V4L2): Nur mit dem Feature
mjpegverfügbar. Die Dekodierung muss vom Anwender durchgeführt werden.