Communitygithub.com

shiguredo/shiguredo-video-device

Referencia de funcionalidades y API de la librería de captura de cámara multiplataforma shiguredo_video_device de Shiguredo. Para consultas sobre enumeración de dispositivos y captura de fotogramas en macOS (AVFoundation), Linux (V4L2/PipeWire) y Windows (Media Foundation), selección de backend mediante features, formatos de píxel y diseño de memoria de fotogramas, restricciones de callbacks y diferencias de comportamiento entre backends.

¿Qué es shiguredo-video-device?

Esta habilidad proporciona acceso completo a la documentación y API de `shiguredo_video_device`, una librería Rust multiplataforma para enumerar cámaras y capturar fotogramas de video. Soporta macOS (AVFoundation), Linux (V4L2 y PipeWire) y Windows (Media Foundation), ofreciendo una API unificada con selección de backend mediante features de Cargo. La librería permite enumerar dispositivos, configurar la captura (resolución, FPS, formato de píxel) y recibir fotogramas a través de callbacks en el hilo del backend. Es útil para desarrolladores que trabajan con visión artificial, streaming o procesamiento de video en Rust, ya que abstrae las diferencias de cada plataforma. Incluye información detallada sobre formatos de píxel (NV12, YUY2, I420, MJPEG), diseño de memoria de los fotogramas, restricciones de los callbacks (no bloquear, no hacer panic) y diferencias de comportamiento entre backends como la obtención de permisos en macOS o la dependencia de COM en Windows.

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

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

shiguredo_video_device

Librería que proporciona enumeración de cámaras y captura de fotogramas en macOS / Linux / Windows mediante una API Rust común para todas las plataformas.

Características

  • Multiplataforma: macOS (AVFoundation), Linux (V4L2 / PipeWire) y Windows (Media Foundation) con la misma API
  • Selección de backend mediante features: El backend habilitado y el predeterminado se eligen con features de Cargo
  • Orientado a callbacks: Durante la captura, se invoca un callback por cada fotograma en el hilo del backend
  • Formatos de píxel: NV12 / YUY2 / I420; en Linux (V4L2) también paso directo de MJPEG (feature mjpeg)
  • Pocas dependencias: La única crate de Rust dependiente es windows (feature mf) en Windows
  • Referencia CVPixelBuffer (macOS): El fotograma incluye una referencia (PixelBuffer) al CVPixelBuffer, que se puede pasar a VideoToolbox sin copiar

Información de versión

  • Nombre de la crate: shiguredo_video_device
  • Versión: 2026.4.0
  • Rust Edition: 2024
  • Versión mínima de Rust: 1.95
  • Versión mínima de macOS: 14.0 (porque referencia AVCaptureDeviceTypeExternal; no funciona en macOS 13 o anterior)
  • Licencia: Apache-2.0
  • Repositorio: https://github.com/shiguredo/video-device-rs

Error y PixelFormat no tienen #[non_exhaustive]. Se pueden enumerar todas las variantes con match.

Instalación

[dependencies]
shiguredo_video_device = "2026.4"

Por defecto, el backend predeterminado es AVFoundation en macOS, V4L2 en Linux y Media Foundation en Windows. Las features para un SO diferente al de compilación se ignoran, por lo que no es necesario separar dependencias por SO.

Features

FeatureSO objetivoDescripción
avfmacOSHabilita el backend AVFoundation
v4l2LinuxHabilita el backend V4L2
pipewireLinuxHabilita el backend PipeWire (enlaza dinámicamente con libpipewire-0.3)
mfWindowsHabilita el backend Media Foundation (depende de la crate windows)
mjpegLinuxHabilita la captura paso directo de MJPEG en V4L2 (incluye v4l2)
default-avfmacOSEstablece AVFoundation como backend predeterminado (incluye avf)
default-v4l2LinuxEstablece V4L2 como backend predeterminado (incluye v4l2)
default-pipewireLinuxEstablece PipeWire como backend predeterminado (incluye pipewire)
default-mfWindowsEstablece Media Foundation como backend predeterminado (incluye mf)

Las features por defecto son default-avf / default-v4l2 / default-mf.

  • Si se habilitan dos o más default-* en el mismo SO, el script de compilación entrará en pánico y no se podrá compilar.
  • Para usar PipeWire como predeterminado en Linux, se necesita default-features = false. Esto también desactiva los predeterminados de macOS y Windows, por lo que en aplicaciones multiplataforma hay que volver a especificar default-avf / default-mf.
  • En un SO sin default-*, VideoDeviceList::enumerate() y VideoCapture::new() no existen. Se deben usar las API explícitas como enumerate_avf() o new_v4l2().
  • Si no hay ningún backend habilitado para un SO, no se exportan VideoCapture, VideoDevice ni VideoDeviceList (solo quedan los tipos Error, PixelFormat, etc.).
  • En Linux se pueden habilitar v4l2 y pipewire simultáneamente.
  • Los cfg enable_* internos de la crate los establece el script de compilación solo para esta crate y no son accesibles desde fuera. El código usuario debe usar #[cfg(target_os = "linux")] y sus propias features para bifurcar.

Para usar PipeWire como predeterminado en Linux y mantener los predeterminados en macOS y Windows:

[dependencies.shiguredo_video_device]
version = "2026.4"
default-features = false
features = ["default-avf", "default-mf", "default-pipewire"]

Para mantener V4L2 como predeterminado pero también habilitar PipeWire y MJPEG:

[dependencies]
shiguredo_video_device = { version = "2026.4", features = ["pipewire", "mjpeg"] }

Requisitos de compilación

SORequisitos
macOSXcode Command Line Tools (incluye libclang para bindgen)
Linux (V4L2)libclang (en Ubuntu: libclang-dev)
Linux (PipeWire)libclang, libpipewire-0.3-dev y pkg-config
WindowsSin dependencias adicionales

API principal

Tipos públicos

TipoDescripciónAPI principal
VideoDeviceListResultado de enumerar dispositivosenumerate(), enumerate_avf(), enumerate_v4l2(), enumerate_pipewire(), enumerate_mf(), as_slice(), len(), is_empty(), &VideoDeviceList: IntoIterator
VideoDeviceUn dispositivoname() -> Result<String>, unique_id() -> Result<String>, formats() -> Vec<VideoFormat>, format_count() -> usize
VideoFormatFormato reportado por el dispositivowidth: i32, height: i32, min_fps: f32, max_fps: f32, pixel_format: PixelFormat
VideoCaptureSesión de capturanew(), new_avf(), new_v4l2(), new_pipewire(), new_mf(), start(), stop(), config()
VideoCaptureConfigConfiguración de captura (implementa Default)device_id: Option<String>, width: i32, height: i32, fps: i32, pixel_format: Option<PixelFormat>
VideoFrame<'a>Fotograma prestado que se pasa al callbackdata, uv_data, width, height, stride, stride_uv, pixel_format, timestamp_us, pixel_buffer, to_owned()
VideoFrameOwnedFotograma propioMismos campos que VideoFrame (almacenados en Vec<u8>), as_frame()
PixelFormatFormato de píxelNv12, Yuy2, I420, Mjpeg, Unknown(u32), name(), to_raw(), Display
PixelBufferReferencia a CVPixelBuffer en macOSas_ptr() -> *mut c_void (Clone hace CFRetain, Drop hace CFRelease)
Error / Result<T>Tipo de error y tipo resultadoVer la sección "Tipo Error"

enumerate() y new() usan el backend predeterminado (default-*). enumerate_*() y new_*() solo existen si el SO objetivo coincide y la feature correspondiente está habilitada.

VideoDeviceList / VideoDevice

  • VideoDevice solo se puede obtener como referencia prestada (&VideoDevice) desde VideoDeviceList. Si se necesita la información después de descartar la lista, hay que copiar el valor de unique_id() u otros campos.
  • El valor de unique_id() se puede pasar directamente a VideoCaptureConfig::device_id (limitado al mismo backend).
  • name() y unique_id() devuelven Result porque en los backends FFI (macOS / Linux) el lado C puede devolver NULL o cadenas no UTF-8. En Windows no fallan.
  • format_count() es el número de entradas reportado por el lado C, que puede ser mayor que formats().len() porque algunas entradas no se pudieron obtener.
  • min_fps y max_fps de VideoFormat son valores aproximados que dependen del backend (ver la sección "Diferencias de comportamiento entre backends").

Ciclo de vida de VideoCapture

  • new*() solo crea la sesión, no inicia la captura (en macOS, si los permisos no están determinados, muestra el diálogo del sistema y no retorna hasta que el usuario responda).
  • start() es idempotente: si ya está en ejecución, devuelve Ok(()). Se permite volver a llamar start() después de stop().
  • stop() es bloqueante: espera a que el hilo de captura y el callback terminen antes de retornar. Si no está en ejecución, no hace nada.
  • Al hacer drop, si está en ejecución, realiza una parada bloqueante como stop() y luego destruye la sesión.
  • config() devuelve los valores solicitados en new*(). No son los valores reales negociados; para resolución, formato y stride reales, consultar los campos de cada fotograma.

VideoCaptureConfig

CampoValor por defectoDescripción
device_idNoneValor de VideoDevice::unique_id(). None usa el dispositivo predeterminado
width640Ancho solicitado
height480Alto solicitado
fps30Fotogramas por segundo solicitados
pixel_formatNoneFormato de píxel solicitado. None deja que el backend elija
  • En Windows, si width, height o fps son <= 0, se devuelve Error::InvalidCaptureConfig. Otros backends no validan desde Rust.
  • pixel_format: Some(PixelFormat::Unknown(_)) devuelve Error::UnsupportedPixelFormat en todos los backends.
  • pixel_format: Some(PixelFormat::Mjpeg) solo funciona en Linux V4L2 con la feature mjpeg. En cualquier otro caso devuelve Error::UnsupportedPixelFormat(PixelFormat::Mjpeg).
  • Los valores solicitados son solo una preferencia; los valores reales los determinan el backend y el dispositivo.

PixelFormat

VarianteContenidoto_raw() (FourCC)
Nv12YUV 4:2:0 semiplano (plano Y + plano UV entrelazado)0x3231564E (NV12)
Yuy2YUV 4:2:2 empaquetado (Y0 U Y1 V)0x32595559 (YUY2)
I420YUV 4:2:0 planar (3 planos: Y / U / V)0x30323449 (I420)
MjpegMotion JPEG. La decodificación es responsabilidad del usuario0x47504A4D (MJPG)
Unknown(u32)FourCC desconocidoEl valor almacenado
  • name() devuelve "NV12", "YUY2", "I420", "MJPEG" o "Unknown". Display para Unknown incluye el valor en formato Unknown(0x????????).
  • Actualmente los backends no generan Unknown. Los formatos desconocidos se excluyen de formats() y sus fotogramas no se pasan al callback.

Diseño de memoria de los fotogramas

Formatodatauv_datastridestride_uv
NV12Plano YSome: plano UV entrelazado (ceil(height / 2) filas)Bytes por fila del plano YBytes por fila del plano UV
I420Plano YSome: regiones contiguas: plano U seguido de plano V (cada uno de stride_uv * ceil(height / 2) bytes)Bytes por fila del plano YBytes por fila de cada plano U / V
YUY2Datos empaquetadosNoneBytes por fila0
MJPEGCarga útil JPEG (longitud en data.len())None00
  • stride puede incluir relleno al final de la fila. Si se necesita un búfer contiguo sin stride, hay que reempaquetar fila por fila (ver pack_frame en "Ejemplos de código").
  • data.len() para NV12 / I420 / YUY2 es stride * height en backends FFI (macOS / Linux) y width * height (YUY2: width * 2 * height) en Windows.
  • timestamp_us está en microsegundos, pero el origen varía según el backend. Solo se debe usar para tiempos relativos entre fotogramas, no como hora de reloj de pared.
  • pixel_buffer es Some en macOS y None en el resto.

Restricciones del callback de fotogramas

El tipo del callback es F: Fn(VideoFrame<'_>) + Send + 'static.

  • Los slices de VideoFrame solo son válidos durante la ejecución del callback. Para usarlos después, copiar con frame.to_owned() a VideoFrameOwned.
  • El callback se ejecuta en el hilo de captura del backend. Las operaciones pesadas pueden provocar pérdida de fotogramas; se recomienda usar sync_channel + try_send para pasar los datos a otro hilo.
  • Como no es FnMut, para actualizar el estado se necesita mutabilidad interior (Cell, RefCell, Mutex, atómicos, etc.). No requiere Sync, por lo que el callback nunca se invoca concurrentemente.
  • El callback no debe entrar en pánico. En macOS y Linux puede abortar el proceso. En Windows, el hilo de captura finaliza y un start() posterior a stop() devuelve Error::CaptureFaulted (sin stop() intermedio, start() lo considera en ejecución y devuelve Ok(())). La librería no captura el pánico con catch_unwind.
  • No se debe llamar a stop() ni hacer drop de VideoCapture desde el callback. En Linux (V4L2 / PipeWire) puede causar use-after-free, y en macOS, un interbloqueo. En Windows, VideoCapture no es Send, por lo que safe Rust impide pasarlo al callback. Para detener, notificar a otro hilo mediante un canal o bandera, y que ese hilo llame a stop().

Seguridad en hilos

TipoSendSyncNotas
VideoDeviceList / VideoDeviceSíSí
VideoCapture (macOS / Linux)SíNo
VideoCapture (Windows)NoNoPorque contiene un guardia de inicialización COM; debe usarse y dropearse en el mismo hilo que lo creó
VideoFrameOwned / PixelBufferSíNoArc<VideoFrameOwned> no se puede compartir entre hilos. Para distribuir a varios receptores, clonar o envolver en Mutex
ErrorSíSíCopy

Ejemplos de código

Enumeración de dispositivos

use shiguredo_video_device::VideoDeviceList;

fn main() -> shiguredo_video_device::Result<()> {
    // Enumerar dispositivos con el backend predeterminado
    let devices = VideoDeviceList::enumerate()?;
    for device in &devices {
        // name() / unique_id() pueden fallar en backends FFI, por eso devuelven 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(())
}

Recibir fotogramas a través de un canal (patrón recomendado)

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 con límite para aplicar contrapresión. Un canal sin límite haría crecer la memoria si el receptor va lento
    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<'_>| {
        // Los slices solo son válidos durante el callback, así que copiamos con to_owned() antes de enviar
        // Si el receptor está saturado, descartamos el fotograma sin bloquear
        let _ = tx.try_send(frame.to_owned());
    })?;

    capture.start()?;

    for _ in 0..30 {
        let frame = rx.recv_timeout(Duration::from_secs(5))?;
        // La resolución y formato reales están en el fotograma (config() solo devuelve los valores solicitados)
        println!(
            "{}x{} {} ts={}us",
            frame.width, frame.height, frame.pixel_format, frame.timestamp_us
        );
    }

    // stop() espera a que los callbacks terminen antes de retornar. No se debe llamar desde el callback
    capture.stop();
    Ok(())
}

Especificar dispositivo y formato

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 se encontró ningún dispositivo de video".into());
    };

    // Si el dispositivo reporta NV12 a 1280x720, lo solicitamos; si no, dejamos que el backend decida
    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 {
        // El valor de unique_id() se puede pasar directamente a 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!("el formato de píxel {format} no es compatible").into())
        }
        Err(e) => Err(e.into()),
    }
}

Eliminar el stride para obtener un búfer contiguo

Útil cuando un codificador u otro componente requiere un búfer contiguo sin relleno.

use shiguredo_video_device::{PixelFormat, VideoFrame};

/// Concatena solo los bytes válidos de cada fila de un plano con stride
fn pack_plane(src: &[u8], stride: usize, row_bytes: usize, rows: usize) -> Option<Vec<u8>> {
    if row_bytes > stride {
        return None;
    }
    // Las dimensiones vienen del dispositivo y no son fiables, por lo que no preasignamos
    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)
}

/// Empaqueta el fotograma en un búfer contiguo sin stride. Devuelve None si las dimensiones son inconsistentes
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 y V se alternan, por lo que los bytes válidos por fila son 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 contiene el plano U seguido inmediatamente del plano 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: 4 bytes por cada 2 píxeles
        PixelFormat::Yuy2 => pack_plane(frame.data, stride, chroma_w * 4, h),
        // MJPEG: carga útil JPEG sin stride
        PixelFormat::Mjpeg => Some(frame.data.to_vec()),
        PixelFormat::Unknown(_) => None,
    }
}

Especificar un backend distinto al predeterminado (Linux PipeWire)

// Se asume que la feature pipewire está habilitada en las dependencias
// Para evitar importaciones no usadas en otros SO, se añade cfg al use
#[cfg(target_os = "linux")]
use shiguredo_video_device::VideoCapture;

#[cfg(target_os = "linux")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut capture = VideoCapture::new_pipewire(
        VideoCaptureConfig::default(),
        |frame| println!("fotograma recibido: {}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!("Este ejemplo solo funciona en Linux");
}

Tipo Error

pub enum Error {
    /// El backend no pudo enumerar dispositivos
    Enumerate(&'static str),
    /// No se pudo abrir el dispositivo
    OpenDevice(&'static str),
    /// No se pudo iniciar la captura
    StartCapture(&'static str),
    /// No se pudo detener la captura
    StopCapture(&'static str),
    /// No se pudo crear la sesión
    CreateSession(&'static str),
    /// Fallo en la configuración de captura
    InvalidCaptureConfig(&'static str),
    /// Formato de píxel no soportado
    UnsupportedPixelFormat(PixelFormat),
    /// La captura falló; se requiere stop() antes de volver a start()
    CaptureFaulted,
}

pub type Result<T> = std::result::Result<T, Error>;

Error implementa std::error::Error, Display y Copy. Los mensajes de error de los backends son literales de cadena estáticos (&'static str).

Diferencias de comportamiento entre backends

macOS (AVFoundation)

  • Permisos: new*() solicita permiso de cámara con AVCaptureDevice.requestAccess(for: .video). Si el usuario deniega, el callback nunca se invoca y start() devuelve Ok(()). No hay forma de detectar la denegación desde esta librería; el desarrollador debe comprobar AVCaptureDevice.authorizationStatus(for: .video) antes de crear la sesión.
  • CVPixelBuffer: Los fotogramas contienen pixel_buffer: Some(PixelBuffer(...)). Se puede pasar a VideoToolbox (por ejemplo, VTCompressionSessionEncodeFrame) sin copiar.
  • Reinicio: Después de stop(), se puede volver a start() sin problema.
  • Dispositivo predeterminado: device_id: None usa la cámara integrada (AVCaptureDevice.default(.external, ...)). Si no hay ninguna cámara externa, usa la integrada (AVCaptureDevice.default(.builtInWideAngleCamera, ...)).

Linux (V4L2)

  • mjpeg feature: Si está habilitada y el dispositivo lo soporta, se puede capturar en MJPEG. La carga útil del fotograma son datos JPEG sin procesar.
  • Resolución: V4L2 puede redondear la resolución a la más cercana soportada. La resolución real se obtiene del fotograma.
  • Dispositivo predeterminado: device_id: None usa /dev/video0.
  • Eliminación de formato: Los formatos que fallan al consultar (VIDIOC_ENUM_FMT) con EIO se omiten.

Linux (PipeWire)

  • Requisitos del sistema: Se necesita un servicio PipeWire en ejecución.
  • Enumeración: Usa la API de PipeWire para listar dispositivos.
  • Dispositivo predeterminado: device_id: None usa el primer dispositivo encontrado (orden de PipeWire).

Windows (Media Foundation)

  • Validación de parámetros: width, height y fps deben ser > 0, de lo contrario Error::InvalidCaptureConfig.
  • COM: El hilo que crea VideoCapture debe haber inicializado COM con CoInitializeEx. El tipo VideoCapture contiene un guardia que asegura COM en el mismo hilo; por eso no es Send.
  • Dispositivo predeterminado: device_id: None usa el primer dispositivo de la lista de Media Foundation.
  • Fotogramas: VideoFrame no contiene pixel_buffer (es None).

Licencia

Apache-2.0

Skills relacionados