Communitygithub.com

shiguredo/shiguredo-video-device

Funktions- und API-Referenz für die plattformübergreifende Kamera-Capture-Bibliothek shiguredo_video_device von Shiguredo.

Was ist shiguredo-video-device?

Diese Skill-Beschreibung für einen KI-Agenten (z. B. Claude Code, Cursor) dokumentiert die Rust-Bibliothek `shiguredo_video_device`. Sie ermöglicht die Kameraaufzählung und Frame-Aufnahme auf macOS (AVFoundation), Linux (V4L2 / PipeWire) und Windows (Media Foundation) über eine einheitliche Rust-API. Die Bibliothek bietet Funktionen zur Geräteabfrage, Konfiguration der Capture-Session, Callback-basierte Frame-Lieferung und behandelt Unterschiede der Backends wie Pixelformate (NV12, YUY2, I420, MJPEG) und Speicherlayouts. Der Skill hilft Entwicklern, die plattformspezifischen Eigenheiten zu verstehen, die richtige Backend-Auswahl über Cargo-Features zu treffen und die API korrekt zu nutzen – inklusive Thread-Safety, Callback-Einschränkungen und Fehlerbehandlung.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/shiguredo/video-device-rs/tree/HEAD/skills/shiguredo-video-device

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

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 (mjpeg Feature)
  • Wenige Abhängigkeiten: Auf Rust-Seite wird nur unter Windows (mf Feature) die windows-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

FeatureZiel-OSBeschreibung
avfmacOSAktiviert das AVFoundation-Backend
v4l2LinuxAktiviert das V4L2-Backend
pipewireLinuxAktiviert das PipeWire-Backend (dynamische Verlinkung mit libpipewire-0.3)
mfWindowsAktiviert das Media Foundation-Backend (Abhängigkeit von der windows-Crate)
mjpegLinuxAktiviert MJPEG-Passthrough unter V4L2 (beinhaltet v4l2)
default-avfmacOSSetzt AVFoundation als Standard-Backend (beinhaltet avf)
default-v4l2LinuxSetzt V4L2 als Standard-Backend (beinhaltet v4l2)
default-pipewireLinuxSetzt PipeWire als Standard-Backend (beinhaltet pipewire)
default-mfWindowsSetzt 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 = false gesetzt werden. Dann entfallen auch die macOS-/Windows-Standards; in plattformübergreifenden Anwendungen müssen default-avf / default-mf wieder explizit angegeben werden.
  • Auf einem OS ohne default-*-Feature existieren VideoDeviceList::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 / VideoDeviceList nicht exportiert (nur Error / PixelFormat usw. bleiben sichtbar).
  • Unter Linux können v4l2 und pipewire gleichzeitig 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

OSAnforderung
macOSXcode Command Line Tools (enthält libclang für bindgen)
Linux (V4L2)libclang (Ubuntu: libclang-dev)
Linux (PipeWire)libclang, libpipewire-0.3-dev, pkg-config
WindowsKeine zusätzlichen Pakete erforderlich

Kern-API

Exportierte Typen

TypBeschreibungHaupt-API
VideoDeviceListErgebnis der Geräteaufzählungenumerate(), enumerate_avf(), enumerate_v4l2(), enumerate_pipewire(), enumerate_mf(), as_slice(), len(), is_empty(), &VideoDeviceList: IntoIterator
VideoDeviceEin einzelnes Gerätname() -> Result<String>, unique_id() -> Result<String>, formats() -> Vec<VideoFormat>, format_count() -> usize
VideoFormatEin vom Gerät gemeldetes Formatwidth: i32, height: i32, min_fps: f32, max_fps: f32, pixel_format: PixelFormat
VideoCaptureEine Capture-Sessionnew(), new_avf(), new_v4l2(), new_pipewire(), new_mf(), start(), stop(), config()
VideoCaptureConfigCapture-Konfiguration (implementiert Default)device_id: Option<String>, width: i32, height: i32, fps: i32, pixel_format: Option<PixelFormat>
VideoFrame<'a>Ein geliehener Frame im Callbackdata, uv_data, width, height, stride, stride_uv, pixel_format, timestamp_us, pixel_buffer, to_owned()
VideoFrameOwnedEin eigener FrameGleiche Felder wie VideoFrame (gespeichert als Vec<u8>), as_frame()
PixelFormatPixelformatNv12, Yuy2, I420, Mjpeg, Unknown(u32), name(), to_raw(), Display
PixelBufferReferenz auf einen macOS-CVPixelBufferas_ptr() -> *mut c_void (Clone erhöht den Referenzzähler via CFRetain, Drop gibt ihn frei via CFRelease)
Error / Result<T>Fehler- und ErgebnistypenSiehe 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 VideoDevice kann nur als Ausleihe (&VideoDevice) aus einer VideoDeviceList entnommen 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 als VideoCaptureConfig::device_id verwendet werden (allerdings nur innerhalb desselben Backends).
  • name() / unique_id() geben Result zurü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 als formats().len(), da nicht alle Einträge erfolgreich abgerufen werden können.
  • min_fps / max_fps in VideoFormat sind 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, wird Ok(()) zurückgegeben. Ein erneuter start()-Aufruf nach stop() ist erlaubt.
  • stop() blockiert, bis der Capture-Thread und alle Callbacks beendet sind. Wenn die Aufnahme nicht läuft, passiert nichts.
  • Beim drop wird eine laufende Aufnahme blockierend gestoppt (wie stop()), bevor die Session zerstört wird.
  • config() gibt die bei new*() ü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

FeldDefault-WertBeschreibung
device_idNoneWert von VideoDevice::unique_id(). None wählt das Standardgerät.
width640Gewünschte Breite
height480Gewünschte Höhe
fps30Gewünschte Bildrate
pixel_formatNoneGewünschtes Pixelformat. None überlässt die Wahl dem Backend.
  • Unter Windows wird Error::InvalidCaptureConfig zurückgegeben, wenn width / height / fps <= 0 sind. Andere Backends prüfen dies nicht auf Rust-Seite.
  • pixel_format: Some(PixelFormat::Unknown(_)) führt bei allen Backends zu Error::UnsupportedPixelFormat.
  • pixel_format: Some(PixelFormat::Mjpeg) ist nur unter Linux mit V4L2 und dem mjpeg-Feature verfügbar. Ansonsten wird Error::UnsupportedPixelFormat(PixelFormat::Mjpeg) zurückgegeben.
  • Die angeforderten Werte sind nur Wünsche; die tatsächlichen Werte bestimmen Backend und Gerät.

PixelFormat

VarianteInhaltto_raw() (FourCC)
Nv12YUV 4:2:0 Semi-Planar (Y-Ebene + UV-interleaved-Ebene)0x3231564E (NV12)
Yuy2YUV 4:2:2 Packed (Y0 U Y1 V)0x32595559 (YUY2)
I420YUV 4:2:0 Planar (3 Ebenen: Y / U / V)0x30323449 (I420)
MjpegMotion JPEG. Dekodierung obliegt dem Anwender.0x47504A4D (MJPG)
Unknown(u32)Unbekannter FourCCDer gespeicherte Wert
  • name() gibt "NV12" / "YUY2" / "I420" / "MJPEG" / "Unknown" zurück. Die Display-Implementierung gibt bei Unknown den Wert im Format Unknown(0x????????) aus.
  • Aktuelle Backends erzeugen nie Unknown. Unbekannte Formate werden von formats() ausgeschlossen und die entsprechenden Frames nicht an den Callback übergeben.

Speicherlayout von Frames

Formatdatauv_datastridestride_uv
NV12Y-EbeneSome: UV-interleaved-Ebene (ceil(height / 2) Zeilen)Bytezahl einer Y-ZeileBytezahl einer UV-Zeile
I420Y-EbeneSome: UV-interleaved-Ebene – direkt nach der U-Ebene folgt die V-Ebene (jeweils stride_uv * ceil(height / 2) Bytes)Bytezahl einer Y-ZeileBytezahl einer U- bzw. V-Zeile
YUY2Gepackte DatenNoneBytezahl einer Zeile0
MJPEGJPEG-Nutzdaten (Länge in data.len())None00
  • stride kann Padding am Zeilenende enthalten. Wenn ein durchgehender Puffer ohne Padding benötigt wird, müssen die Daten zeilenweise umkopiert werden (siehe pack_frame im Codebeispiel).
  • data.len() für NV12 / I420 / YUY2 beträgt bei den FFI-Backends (macOS/Linux) stride * height, unter Windows width * height (YUY2: width * 2 * height).
  • timestamp_us ist 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_buffer ist auf macOS Some, auf anderen Plattformen None.

Einschränkungen des Frame-Callbacks

Der Callback-Typ ist F: Fn(VideoFrame<'_>) + Send + 'static.

  • Die Slice-Daten von VideoFrame sind nur innerhalb des Callbacks gültig. Sollen sie später verwendet werden, muss mit frame.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_send an einen anderen Thread delegiert werden.
  • Da der Callback Fn (nicht FnMut) ist, muss für Zustandsänderungen innere Veränderbarkeit verwendet werden (Cell / RefCell / Mutex / atomare Typen). Der Callback wird nicht parallel aufgerufen (Sync ist 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 einem stop()) geben Error::CaptureFaulted zurück. Die Bibliothek fängt Panics nicht mit catch_unwind ab.
  • Innerhalb des Callbacks darf weder stop() aufgerufen noch VideoCapture gedroppt werden. Unter Linux (V4L2 / PipeWire) kann dies zu Use-after-Free, unter macOS zu Deadlocks führen. Unter Windows ist VideoCapture ohnehin !Send und 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 dann stop() aufruft.

Thread-Sicherheit

TypSendSyncAnmerkungen
VideoDeviceList / VideoDeviceYesYes
VideoCapture (macOS/Linux)YesNo
VideoCapture (Windows)NoNoEnthält einen COM-Initialisierungs-Guard; muss im Thread erstellt und wieder dort gedroppt werden.
VideoFrameOwned / PixelBufferYesNoArc<VideoFrameOwned> kann nicht zwischen Threads geteilt werden. Für mehrere Empfänger entweder klonen oder in einen Mutex einpacken.
ErrorYesYesCopy

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

FehlerBeschreibung
Error::NoBackendKein Backend ist für die aktuelle Plattform aktiviert.
Error::UnsupportedPixelFormat(PixelFormat)Das angeforderte Pixelformat wird nicht unterstützt.
Error::InvalidCaptureConfigUngültige Konfiguration (z. B. negative Maße unter Windows).
Error::NoDeviceKein Gerät gefunden.
Error::CaptureFailed(String)Allgemeiner Fehler bei der Aufnahme.
Error::CaptureFaultedDie Aufnahme ist aufgrund eines Fehlers abgestürzt.

Backend-Unterschiede

  • macOS (AVFoundation): Erfordert macOS 14.0+. min_fps und max_fps sind Schätzwerte.
  • Linux V4L2: min_fps und max_fps werden 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): VideoCapture ist !Send. min_fps und max_fps sind Schätzwerte.
  • MJPEG (V4L2): Nur mit dem Feature mjpeg verfügbar. Die Dekodierung muss vom Anwender durchgeführt werden.

Verwandte Skills