shiguredo_video_device
Bibliothèque fournissant l'énumération des caméras et la capture d'images sur macOS / Linux / Windows via une API Rust commune à toutes les plateformes.
Caractéristiques
- Multiplateforme : macOS (AVFoundation), Linux (V4L2 / PipeWire), Windows (Media Foundation) avec une API identique
- Sélection du backend par fonctionnalité : Choisissez le backend activé et le backend par défaut via les fonctionnalités Cargo
- Piloté par callback : Pendant la capture, un callback est appelé pour chaque image sur le thread du backend
- Formats de pixels : NV12 / YUY2 / I420, et passage MJPEG sur Linux (V4L2) avec la fonctionnalité
mjpeg - Peu de dépendances : La seule crate Rust dépendante est
windows(pour la fonctionnalitémfsur Windows) - Référence CVPixelBuffer (macOS) : Les frames incluent une référence au
CVPixelBuffer(PixelBuffer), permettant de les passer à VideoToolbox sans copie
Informations de version
- Nom de la crate :
shiguredo_video_device - Version : 2026.4.0
- Édition Rust : 2024
- Version minimale de Rust : 1.95
- Version minimale de macOS : 14.0 (nécessite
AVCaptureDeviceTypeExternal, ne fonctionne pas sur macOS 13 et antérieur) - Licence : Apache-2.0
- Dépôt : https://github.com/shiguredo/video-device-rs
Error / PixelFormat ne sont pas #[non_exhaustive]. Tous les variants peuvent être énumérés avec match.
Installation
[dependencies]
shiguredo_video_device = "2026.4"
Par défaut, le backend par défaut est AVFoundation sur macOS, V4L2 sur Linux et Media Foundation sur Windows. Les fonctionnalités pour un système d'exploitation autre que celui de la cible de compilation sont ignorées, il n'est donc pas nécessaire de séparer les dépendances par OS.
Fonctionnalités
| Fonctionnalité | OS cible | Description |
|---|---|---|
avf | macOS | Active le backend AVFoundation |
v4l2 | Linux | Active le backend V4L2 |
pipewire | Linux | Active le backend PipeWire (lien dynamique avec libpipewire-0.3) |
mf | Windows | Active le backend Media Foundation (dépend de la crate windows) |
mjpeg | Linux | Active le passage MJPEG sur V4L2 (inclut v4l2) |
default-avf | macOS | Définit AVFoundation comme backend par défaut (inclut avf) |
default-v4l2 | Linux | Définit V4L2 comme backend par défaut (inclut v4l2) |
default-pipewire | Linux | Définit PipeWire comme backend par défaut (inclut pipewire) |
default-mf | Windows | Définit Media Foundation comme backend par défaut (inclut mf) |
Les fonctionnalités par défaut sont default-avf / default-v4l2 / default-mf.
- Activer deux
default-*ou plus sur le même OS provoquera une panique du script de build - Pour utiliser PipeWire par défaut sur Linux, il faut
default-features = false. Cela désactive également les valeurs par défaut sur macOS/Windows, donc pour une application multiplateforme, il faut réactiverdefault-avf/default-mf - Sur un OS sans
default-*,VideoDeviceList::enumerate()/VideoCapture::new()n'existent pas. Utilisez les API explicites commeenumerate_avf()/new_v4l2() - Sur un OS où aucun backend n'est activé,
VideoCapture/VideoDevice/VideoDeviceListne sont pas exposés (seuls les types commeError/PixelFormatrestent) - Sur Linux,
v4l2etpipewirepeuvent être activés simultanément - Les cfg
enable_*internes de la crate sont définis par le script de build uniquement pour cette crate et ne sont pas accessibles depuis l'extérieur. L'utilisateur doit se baser sur#[cfg(target_os = "linux")]et ses propres fonctionnalités
Pour utiliser PipeWire comme backend par défaut sur Linux tout en conservant les valeurs par défaut sur macOS et Windows :
[dependencies.shiguredo_video_device]
version = "2026.4"
default-features = false
features = ["default-avf", "default-mf", "default-pipewire"]
Pour conserver V4L2 par défaut tout en activant PipeWire et MJPEG :
[dependencies]
shiguredo_video_device = { version = "2026.4", features = ["pipewire", "mjpeg"] }
Prérequis de compilation
| OS | Prérequis |
|---|---|
| macOS | Xcode Command Line Tools (inclut libclang pour bindgen) |
| Linux (V4L2) | libclang (sur Ubuntu : libclang-dev) |
| Linux (PipeWire) | libclang et libpipewire-0.3-dev, pkg-config |
| Windows | Aucun paquet supplémentaire requis |
API principale
Types exposés
| Type | Description | API principale |
|---|---|---|
VideoDeviceList | Résultat de l'énumération des périphériques | enumerate(), enumerate_avf(), enumerate_v4l2(), enumerate_pipewire(), enumerate_mf(), as_slice(), len(), is_empty(), &VideoDeviceList: IntoIterator |
VideoDevice | Un seul périphérique | name() -> Result<String>, unique_id() -> Result<String>, formats() -> Vec<VideoFormat>, format_count() -> usize |
VideoFormat | Format signalé par le périphérique | width: i32, height: i32, min_fps: f32, max_fps: f32, pixel_format: PixelFormat |
VideoCapture | Session de capture | new(), new_avf(), new_v4l2(), new_pipewire(), new_mf(), start(), stop(), config() |
VideoCaptureConfig | Configuration de capture (implémente Default) | device_id: Option<String>, width: i32, height: i32, fps: i32, pixel_format: Option<PixelFormat> |
VideoFrame<'a> | Image empruntée passée au callback | data, uv_data, width, height, stride, stride_uv, pixel_format, timestamp_us, pixel_buffer, to_owned() |
VideoFrameOwned | Image possédée | Mêmes champs que VideoFrame (stockés dans Vec<u8>), as_frame() |
PixelFormat | Format de pixel | Nv12, Yuy2, I420, Mjpeg, Unknown(u32), name(), to_raw(), Display |
PixelBuffer | Référence à un CVPixelBuffer (macOS) | as_ptr() -> *mut c_void (Clone fait CFRetain, Drop fait CFRelease) |
Error / Result<T> | Type d'erreur et type de résultat | Voir la section « Types d'erreur » |
enumerate() / new() utilisent le backend par défaut (default-*). enumerate_*() / new_*() n'existent que si l'OS cible correspond et que la fonctionnalité associée est activée.
VideoDeviceList / VideoDevice
VideoDevicene peut être extrait que par emprunt (&VideoDevice) depuisVideoDeviceList. Si vous avez besoin des informations après avoir abandonné la liste, copiez des valeurs commeunique_id()- La valeur de
unique_id()peut être passée directement àVideoCaptureConfig::device_id(pour le même backend uniquement) name()/unique_id()renvoient unResultcar le backend FFI (macOS / Linux) peut retourner NULL ou des chaînes non UTF-8. Windows ne peut pas échouerformat_count()correspond au nombre d'entrées signalé par le backend C, qui peut être supérieur àformats().len()(les entrées non récupérables sont exclues)min_fps/max_fpsdeVideoFormatsont des valeurs approximatives dépendant du backend (voir la section « Différences de comportement entre les backends »)
Cycle de vie de VideoCapture
new*()crée la session sans démarrer la capture (sur macOS, si l'autorisation n'est pas encore déterminée, une boîte de dialogue système apparaît et la fonction ne retourne qu'après la réponse de l'utilisateur)start()est idempotent : si la capture est déjà en cours, il renvoieOk(()). Un nouveaustart()aprèsstop()est autoriséstop()est bloquant : il attend la fin du thread de capture et la complétion de tous les callbacks avant de retourner. Ne fait rien si la capture n'est pas en cours- Lors du
drop, si la capture est en cours, un arrêt bloquant (identique àstop()) est effectué avant de détruire la session config()renvoie les valeurs demandées lors denew*(). Il ne s'agit pas des valeurs réelles après négociation : pour la résolution, le format et le stride, référez-vous aux champs de chaque frame
VideoCaptureConfig
| Champ | Valeur par défaut | Description |
|---|---|---|
device_id | None | Valeur de VideoDevice::unique_id(). None signifie le périphérique par défaut |
width | 640 | Largeur demandée |
height | 480 | Hauteur demandée |
fps | 30 | Fréquence d'images demandée |
pixel_format | None | Format de pixel demandé. None laisse le backend choisir |
- Sur Windows, si
width/height/fpssont inférieurs ou égaux à zéro, une erreurError::InvalidCaptureConfigest renvoyée. Les autres backends ne vérifient pas ces valeurs côté Rust pixel_format: Some(PixelFormat::Unknown(_))renvoieError::UnsupportedPixelFormatsur tous les backendspixel_format: Some(PixelFormat::Mjpeg)n'est utilisable qu'avec Linux V4L2 + la fonctionnalitémjpeg. Sinon, renvoieError::UnsupportedPixelFormat(PixelFormat::Mjpeg)- Les valeurs demandées ne sont que des souhaits : les valeurs réelles sont déterminées par le backend et le périphérique
PixelFormat
| Variant | Contenu | to_raw() (FourCC) |
|---|---|---|
Nv12 | YUV 4:2:0 semi-planaire (plan Y + plan UV entrelacé) | 0x3231564E (NV12) |
Yuy2 | YUV 4:2:2 paquet (Y0 U Y1 V) | 0x32595559 (YUY2) |
I420 | YUV 4:2:0 planaire (3 plans Y / U / V) | 0x30323449 (I420) |
Mjpeg | Motion JPEG. Le décodage est à la charge de l'utilisateur | 0x47504A4D (MJPG) |
Unknown(u32) | FourCC inconnu | La valeur stockée |
name()renvoie"NV12"/"YUY2"/"I420"/"MJPEG"/"Unknown".DisplayafficheUnknown(0x????????)pour le variantUnknown- Actuellement, les backends ne génèrent pas de
Unknown. Les formats inconnus sont exclus deformats()et leurs frames ne sont pas passées au callback
Disposition mémoire des frames
| Format | data | uv_data | stride | stride_uv |
|---|---|---|---|---|
| NV12 | Plan Y | Some : plan UV entrelacé (ceil(height / 2) lignes) | Nombre d'octets par ligne de Y | Nombre d'octets par ligne de UV |
| I420 | Plan Y | Some : zone contiguë avec le plan U suivi du plan V (chacun stride_uv * ceil(height / 2) octets) | Nombre d'octets par ligne de Y | Nombre d'octets par ligne de U/V |
| YUY2 | Données paquet | None | Nombre d'octets par ligne | 0 |
| MJPEG | Payload JPEG (longueur = data.len()) | None | 0 | 0 |
stridepeut inclure le padding en fin de ligne. Si vous avez besoin d'un buffer contigu exact, il faut recompacter ligne par ligne (voirpack_framedans les exemples)data.len()pour NV12 / I420 / YUY2 vautstride * heightsur les backends FFI (macOS / Linux) etwidth * heightsur Windows (YUY2 :width * 2 * height)timestamp_usest en microsecondes, mais son origine varie selon le backend. Utilisez-le uniquement pour des temps relatifs entre frames, pas comme horloge muralepixel_buffervautSomesur macOS,Noneailleurs
Contraintes du callback de frame
Le type du callback est F: Fn(VideoFrame<'_>) + Send + 'static.
- Les slices de
VideoFramene sont valides que pendant l'exécution du callback. Pour les utiliser après, copiez-les avecframe.to_owned()pour obtenir unVideoFrameOwned - Le callback est exécuté sur le thread de capture du backend. Un traitement lourd peut entraîner la perte de frames ; passez les données à un autre thread via
sync_channel+try_send - Il n'est pas
FnMut, donc pour mettre à jour un état, utilisez la mutabilité intérieure (Cell/RefCell/Mutex/ types atomiques). Aucune exigenceSync: le callback ne sera jamais appelé en parallèle - Le callback ne doit pas paniquer. Sur macOS/Linux, cela peut provoquer un abort du processus. Sur Windows, le thread de capture se termine et un
start()aprèsstop()renverraError::CaptureFaulted(unstart()sansstop()intermédiaire est considéré comme déjà en cours et renvoieOk(())). La bibliothèque n'utilise pascatch_unwind - Ne pas appeler
stop()nidropleVideoCapturedepuis le callback. Cela pourrait causer un use-after-free sur Linux (V4L2 / PipeWire) ou un deadlock sur macOS. LeVideoCaptureWindows est!Send, donc en Rust safe, il ne peut pas être passé au callback. Pour arrêter, utilisez un canal ou un flag pour notifier un autre thread, et appelezstop()depuis ce thread
Sécurité des threads
| Type | Send | Sync | Notes |
|---|---|---|---|
VideoDeviceList / VideoDevice | Oui | Oui | |
VideoCapture (macOS / Linux) | Oui | Non | |
VideoCapture (Windows) | Non | Non | Contient un garde d'initialisation COM ; à utiliser et drop sur le thread de création |
VideoFrameOwned / PixelBuffer | Oui | Non | Arc<VideoFrameOwned> ne peut pas être partagé entre threads. Pour distribuer à plusieurs récepteurs, clonez ou encapsulez dans un Mutex |
Error | Oui | Oui | Copy |
Exemples de code
Énumération des périphériques
use shiguredo_video_device::VideoDeviceList;
fn main() -> shiguredo_video_device::Result<()> {
// Énumérer les périphériques avec le backend par défaut
let devices = VideoDeviceList::enumerate()?;
for device in &devices {
// name() / unique_id() peuvent échouer sur les backends FFI, donc renvoient un 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(())
}
Réception des frames via un canal (modèle recommandé)
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>> {
// Canal avec limite pour créer une contre-pression. Un canal illimité ferait croître la mémoire si le récepteur est lent
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<'_>| {
// Les slices ne sont valides que pendant le callback, donc copiez avec to_owned() avant d'envoyer
// Si le récepteur est saturé, ne bloquez pas et jetez la frame
let _ = tx.try_send(frame.to_owned());
})?;
capture.start()?;
for _ in 0..30 {
let frame = rx.recv_timeout(Duration::from_secs(5))?;
// La résolution et le format réels sont ceux de la frame (config() donne les valeurs demandées)
println!(
"{}x{} {} ts={}us",
frame.width, frame.height, frame.pixel_format, frame.timestamp_us
);
}
// stop() attend la fin des callbacks avant de retourner. Ne pas appeler depuis un callback
capture.stop();
Ok(())
}
Spécifier un périphérique et un format
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());
};
// Si le périphérique signale du NV12 en 1280x720, demandez-le ; sinon, laissez le backend choisir
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 {
// La valeur de unique_id() peut être passée directement à 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()),
}
}
Supprimer le stride pour obtenir un buffer contigu
Utile lorsque des encodeurs nécessitent un buffer contigu de la largeur exacte.
use shiguredo_video_device::{PixelFormat, VideoFrame};
/// Concatène les octets utiles de chaque ligne d'un plan avec stride
fn pack_plane(src: &[u8], stride: usize, row_bytes: usize, rows: usize) -> Option<Vec<u8>> {
if row_bytes > stride {
return None;
}
// Les dimensions proviennent du périphérique et ne sont pas fiables ; n'allouez pas à l'avance, agrandissez au fur et à mesure
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)
}
/// Reconditionne une frame en un buffer contigu sans stride. Renvoie None si les dimensions sont incohérentes
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 et V alternent, donc le nombre d'octets utiles par ligne = 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 est une zone contiguë : plan U suivi du plan 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 pixels = 4 octets
PixelFormat::Yuy2 => pack_plane(frame.data, stride, chroma_w * 4, h),
// MJPEG : payload JPEG sans stride
PixelFormat::Mjpeg => Some(frame.data.to_vec()),
PixelFormat::Unknown(_) => None,
}
}
Utiliser un backend non par défaut (Linux PipeWire)
// Suppose que la fonctionnalité pipewire est activée dans les dépendances
// Pour éviter les imports inutilisés sur d'autres OS, ajoutez cfg à use
#[cfg(target_os = "linux")]
use shiguredo_video_device::VideoCapture;
#[cfg(target_os = "linux")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
// new_pipewire() n'existe que sur Linux avec la fonctionnalité pipewire
let mut capture = VideoCapture::new_pipewire(
VideoCaptureConfig::default(),
|frame: VideoFrame<'_>| {
println!("Frame reçue : {}x{}", frame.width, frame.height);
},
)?;
capture.start()?;
std::thread::sleep(std::time::Duration::from_secs(2));
capture.stop();
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn main() {
println!("PipeWire n'est disponible que sous Linux.");
}
Énumération explicite par backend
use shiguredo_video_device::VideoDeviceList;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Énumération explicite : les fonctions enumerate_*() ne sont disponibles que si le backend correspondant est activé
#[cfg(target_os = "macos")]
{
let avf_devices = VideoDeviceList::enumerate_avf()?;
println!("Périphériques AVFoundation : {}", avf_devices.len());
}
#[cfg(target_os = "linux")]
{
let v4l2_devices = VideoDeviceList::enumerate_v4l2()?;
println!("Périphériques V4L2 : {}", v4l2_devices.len());
let pw_devices = VideoDeviceList::enumerate_pipewire()?;
println!("Périphériques PipeWire : {}", pw_devices.len());
}
#[cfg(target_os = "windows")]
{
let mf_devices = VideoDeviceList::enumerate_mf()?;
println!("Périphériques Media Foundation : {}", mf_devices.len());
}
Ok(())
}
Types d'erreur
pub enum Error {
/// L'appel système ou la fonction du backend a échoué avec un message
SystemError(String),
/// Le périphérique spécifié est introuvable lors du démarrage de la capture
DeviceNotFound,
/// Le format de pixel demandé n'est pas supporté
UnsupportedPixelFormat(PixelFormat),
/// La configuration de capture est invalide (par ex. largeur/hauteur/fps <= 0 sur Windows)
InvalidCaptureConfig,
/// Le thread de capture a planté (Windows seulement)
CaptureFaulted,
/// Le format de pixel demandé n'est pas supporté par le périphérique
UnsupportedFormat,
/// Erreur de conversion de chaîne (nom ou ID de périphérique non UTF-8)
StringConversionError,
/// Timeout lors de l'attente de l'autorisation de la caméra sur macOS
PermissionTimeout,
/// Erreur liée à la gestion du thread
ThreadError(String),
/// Erreur liée au pool de threads
ThreadPoolError(String),
/// Autre erreur interne
Other(String),
}
pub type Result<T> = std::result::Result<T, Error>;
Différences de comportement entre les backends
macOS (AVFoundation)
new()peut bloquer pour afficher la boîte de dialogue d'autorisation de la caméra. Si l'utilisateur refuse,start()renvoieError::SystemErrormin_fps/max_fpsdansVideoFormatsont extraits desAVFrameRateRangeduAVCaptureDeviceFormatpixel_bufferdansVideoFramecontient toujoursSome(PixelBuffer)permettant un accès direct auCVPixelBuffer- Les identifiants de périphériques (
unique_id()) sont des UID de type"0x1a2b3c4d5e6f7g8h"
Linux V4L2
- Necessite le paquet
libclang-devpour la compilation - Les formats MJPEG ne sont disponibles qu'avec la fonctionnalité
mjpeg. Sans elle,formats()exclut le format MJPEG etpixel_format: Some(PixelFormat::Mjpeg)renvoieError::UnsupportedPixelFormat min_fps/max_fps: V4L2 ne fournit pas directement les FPS viaVIDIOC_ENUM_FRAMESIZES/VIDIOC_ENUM_FRAMEINTERVALS. Les valeurs sont dérivées des intervalles supportés ; si non disponibles,min_fps=max_fps= 0.0- Les identifiants de périphériques sont des chemins de fichiers comme
/dev/video0 - Les noms de périphériques peuvent contenir des caractères non UTF-8 (provenant du noyau)
Linux PipeWire
- Necessite
libpipewire-0.3-devetpkg-configpour la compilation, etlibpipewire-0.3au runtime min_fps/max_fps: PipeWire ne fournit pas d'informations de FPS via l'API de registre. Les valeurs sont toujours 0.0- Les identifiants de périphériques sont des ID d'objet PipeWire (entier sous forme de chaîne)
- PipeWire peut énumérer des périphériques virtuels (comme les sources d'écran)
Windows Media Foundation
- Ne necessite aucun paquet supplémentaire pour la compilation
VideoCapturen'est pasSenden raison du garde d'initialisation COM. Créez et utilisez le sur le même threadwidth/height/fpsdoivent être > 0 ; sinon,new()renvoieError::InvalidCaptureConfigmin_fps/max_fps: Media Foundation fournit ces informations viaIMFMediaType. Les valeurs sont extraites du type de média du dispositif- Les identifiants de périphériques sont des chaînes de symbole du dispositif (ex.
"\\?\usb#vid_xxxx&pid_yyyy&mi_00#...") - Les noms de périphériques sont toujours en UTF-16 convertis en UTF-8 ; le
Resultdename()ne devrait jamais échouer - Si le thread de capture plante,
start()aprèsstop()renverraError::CaptureFaulted
Licence
Licencié sous Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0.
Sauf obligation légale ou accord écrit, le logiciel distribué sous cette licence est fourni « EN L'ÉTAT », SANS GARANTIE NI CONDITION D'AUCUNE SORTE, explicite ou implicite.