Communitygithub.com

shiguredo/shiguredo-video-device

Biblioteca Rust de captura de câmera multiplataforma da Shiguredo. Referência de funcionalidades e API para enumeração de dispositivos e captura de quadros no macOS (AVFoundation), Linux (V4L2/PipeWire) e Windows (Media Foundation), com seleção de backend via features, formatos de pixel e layout de memória de quadros, restrições de callbacks e diferenças de comportamento entre backends.

O que é shiguredo-video-device?

Esta skill descreve a biblioteca `shiguredo_video_device`, uma solução Rust multiplataforma para enumerar câmeras e capturar quadros de vídeo no macOS, Linux e Windows usando uma API unificada. A biblioteca permite selecionar o backend de captura através de features (AVFoundation, V4L2, PipeWire, Media Foundation), suporta múltiplos formatos de pixel (NV12, YUY2, I420, MJPEG) e fornece callbacks para processamento de quadros. É útil para desenvolvedores que precisam de captura de vídeo de alto desempenho e baixo nível em aplicações Rust, oferecendo controle sobre dispositivos, resoluções, taxas de quadros e formatos de pixel, com suporte para stride, timestamps e, no macOS, acesso direto ao CVPixelBuffer. A skill cobre a configuração de dependências, a enumeração de dispositivos, a inicialização de sessões de captura, o mapeamento de memória dos quadros e as restrições importantes dos callbacks, além de fornecer exemplos de código para uso prático. É especialmente valiosa para projetos que exigem captura de câmera consistente em múltiplas plataformas sem depender de bibliotecas externas complexas.

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

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

shiguredo_video_device

Biblioteca que fornece enumeração de câmeras e captura de quadros no macOS / Linux / Windows através de uma API Rust comum entre plataformas.

Características

  • Multiplataforma: lida com macOS (AVFoundation), Linux (V4L2 / PipeWire) e Windows (Media Foundation) usando a mesma API
  • Seleção de backend via features: escolha o backend ativado e o padrão através de features
  • Orientado a callbacks: durante a captura, um callback é chamado para cada quadro na thread do backend
  • Formatos de pixel: NV12 / YUY2 / I420, e passagem MJPEG no Linux (V4L2) com a feature mjpeg
  • Poucas dependências: a única crate Rust dependente é a windows (com a feature mf) no Windows
  • Referência CVPixelBuffer (macOS): o quadro inclui uma referência (PixelBuffer) ao CVPixelBuffer, que pode ser passada para o VideoToolbox sem cópia

Informações de versão

  • Nome da crate: shiguredo_video_device
  • Versão: 2026.4.0
  • Rust Edition: 2024
  • Versão mínima do Rust: 1.95
  • Versão mínima do macOS: 14.0 (consulta AVCaptureDeviceTypeExternal; não funciona no macOS 13 ou anterior)
  • Licença: Apache-2.0
  • Repositório: https://github.com/shiguredo/video-device-rs

Error / PixelFormat não possuem #[non_exhaustive]. Todos os variantes podem ser enumerados com match.

Introdução

[dependencies]
shiguredo_video_device = "2026.4"

Por padrão, o macOS usa AVFoundation, o Linux usa V4L2 e o Windows usa Media Foundation como backend padrão. Features de SOs diferentes do alvo de compilação são ignoradas, portanto não é necessário separar dependências por SO.

Features

featureSO alvoDescrição
avfmacOSAtiva o backend AVFoundation
v4l2LinuxAtiva o backend V4L2
pipewireLinuxAtiva o backend PipeWire (link dinâmico com libpipewire-0.3)
mfWindowsAtiva o backend Media Foundation (depende da crate windows)
mjpegLinuxAtiva a captura com passagem MJPEG no V4L2 (inclui v4l2)
default-avfmacOSDefine AVFoundation como backend padrão (inclui avf)
default-v4l2LinuxDefine V4L2 como backend padrão (inclui v4l2)
default-pipewireLinuxDefine PipeWire como backend padrão (inclui pipewire)
default-mfWindowsDefine Media Foundation como backend padrão (inclui mf)

As features padrão são default-avf / default-v4l2 / default-mf.

  • Se mais de uma feature default-* for ativada no mesmo SO, o script de build entrará em pânico e não compilará
  • Para usar PipeWire como padrão no Linux, é necessário default-features = false. Isso também remove os padrões do macOS e Windows; portanto, em aplicativos multiplataforma, é necessário reespecificar default-avf / default-mf
  • Em SOs sem nenhum default-*, VideoDeviceList::enumerate() e VideoCapture::new() não existem. Use as APIs explícitas como enumerate_avf() / new_v4l2()
  • Se nenhum backend estiver ativado para um SO, VideoCapture, VideoDevice e VideoDeviceList não serão exportados (apenas tipos como Error / PixelFormat permanecerão)
  • No Linux, v4l2 e pipewire podem ser ativados simultaneamente
  • Os cfgs enable_* internos da crate são configurados apenas pelo script de build e não podem ser referenciados por usuários da crate. O usuário deve usar #[cfg(target_os = "linux")] e suas próprias features para ramificar

Para usar PipeWire como padrão no Linux, mantendo os padrões do macOS e Windows:

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

Para manter V4L2 como padrão, mas também habilitar PipeWire e MJPEG:

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

Requisitos de build

SORequisitos
macOSXcode Command Line Tools (inclui libclang para bindgen)
Linux (V4L2)libclang (no Ubuntu: libclang-dev)
Linux (PipeWire)libclang, libpipewire-0.3-dev e pkg-config
WindowsNenhum pacote adicional necessário

API principal

Tipos exportados

TipoDescriçãoAPI principal
VideoDeviceListResultado da enumeração de dispositivosenumerate(), enumerate_avf(), enumerate_v4l2(), enumerate_pipewire(), enumerate_mf(), as_slice(), len(), is_empty(), &VideoDeviceList: IntoIterator
VideoDeviceUm dispositivoname() -> Result<String>, unique_id() -> Result<String>, formats() -> Vec<VideoFormat>, format_count() -> usize
VideoFormatFormato relatado por um dispositivowidth: i32, height: i32, min_fps: f32, max_fps: f32, pixel_format: PixelFormat
VideoCaptureSessão de capturanew(), new_avf(), new_v4l2(), new_pipewire(), new_mf(), start(), stop(), config()
VideoCaptureConfigConfiguração de captura (implementa Default)device_id: Option<String>, width: i32, height: i32, fps: i32, pixel_format: Option<PixelFormat>
VideoFrame<'a>Quadro emprestado passado ao callbackdata, uv_data, width, height, stride, stride_uv, pixel_format, timestamp_us, pixel_buffer, to_owned()
VideoFrameOwnedQuadro proprietárioMesmos campos que VideoFrame (armazenado como Vec<u8>), as_frame()
PixelFormatFormato de pixelNv12, Yuy2, I420, Mjpeg, Unknown(u32), name(), to_raw(), Display
PixelBufferReferência ao CVPixelBuffer no macOSas_ptr() -> *mut c_void (Clone faz CFRetain, Drop faz CFRelease)
Error / Result<T>Tipo de erro e resultadoConsulte a seção "Tipo de erro"

enumerate() / new() usam o backend padrão (default-*). enumerate_*() / new_*() existem apenas quando o SO alvo corresponde e a feature correspondente está ativada.

VideoDeviceList / VideoDevice

  • VideoDevice só pode ser obtido como referência emprestada (&VideoDevice) a partir de VideoDeviceList. Se precisar das informações após descartar a lista, copie valores como unique_id()
  • O valor de unique_id() pode ser passado diretamente para VideoCaptureConfig::device_id (apenas para o mesmo backend)
  • name() / unique_id() retornam Result porque os backends FFI (macOS / Linux) podem retornar NULL ou strings não UTF-8 do lado C. No Windows, nunca falham
  • format_count() é o número de entradas relatadas pelo lado C, que pode ser maior que formats().len() (que exclui entradas não obtidas)
  • min_fps / max_fps de VideoFormat são valores aproximados dependentes do backend (consulte a seção "Diferenças de comportamento entre backends")

Ciclo de vida do VideoCapture

  • new*() apenas cria a sessão; a captura não começa (no macOS, se a permissão ainda não foi determinada, o diálogo do SO é exibido e a chamada bloqueia até a resposta)
  • start() é idempotente: se já estiver em execução, retorna Ok(()). Também é permitido chamar start() novamente após stop()
  • stop() é bloqueante: aguarda a thread de captura e a conclusão do callback antes de retornar. Se não estiver em execução, não faz nada
  • Ao fazer drop da sessão, se estiver em execução, realiza uma parada bloqueante igual a stop() e depois destrói a sessão
  • config() retorna os valores solicitados em new*(), não os valores reais após negociação. Para obter resolução, formato e stride reais, consulte os campos de cada quadro

VideoCaptureConfig

CampoValor padrãoDescrição
device_idNoneValor de VideoDevice::unique_id(). None indica dispositivo padrão
width640Largura solicitada
height480Altura solicitada
fps30Taxa de quadros solicitada
pixel_formatNoneFormato de pixel solicitado. None delega a escolha ao backend
  • No Windows, se width / height / fps forem <= 0, retorna Error::InvalidCaptureConfig. Os outros backends não validam no lado Rust
  • pixel_format: Some(PixelFormat::Unknown(_)) retorna Error::UnsupportedPixelFormat em todos os backends
  • pixel_format: Some(PixelFormat::Mjpeg) só é válido no Linux V4L2 com a feature mjpeg. Caso contrário, retorna Error::UnsupportedPixelFormat(PixelFormat::Mjpeg)
  • Os valores solicitados são apenas desejos; os valores reais são determinados pelo backend e dispositivo

PixelFormat

VarianteConteúdoto_raw() (FourCC)
Nv12YUV 4:2:0 semi-planar (plano Y + plano UV intercalado)0x3231564E (NV12)
Yuy2YUV 4:2:2 empacotado (Y0 U Y1 V)0x32595559 (YUY2)
I420YUV 4:2:0 planar (3 planos: Y / U / V)0x30323449 (I420)
MjpegMotion JPEG. Decodificação é responsabilidade do usuário0x47504A4D (MJPG)
Unknown(u32)FourCC desconhecidoValor armazenado
  • name() retorna "NV12" / "YUY2" / "I420" / "MJPEG" / "Unknown". Display inclui o valor no formato Unknown(0x????????) quando Unknown
  • Atualmente, nenhum backend gera Unknown. Formatos desconhecidos são excluídos de formats() e seus quadros nunca são passados ao callback

Layout de memória dos quadros

Formatodatauv_datastridestride_uv
NV12Plano YSome: plano UV intercalado (altura ceil(height / 2) linhas)Bytes por linha do YBytes por linha do UV
I420Plano YSome: região contígua com plano U seguido pelo plano V (cada um com stride_uv * ceil(height / 2) bytes)Bytes por linha do YBytes por linha de cada U / V
YUY2Dados empacotadosNoneBytes por linha0
MJPEGPayload JPEG (tamanho é data.len())None00
  • stride pode incluir padding no final da linha. Se precisar de um buffer contínuo exato, remonte linha por linha (consulte pack_frame em "Exemplos de código")
  • Em NV12 / I420 / YUY2, data.len() é stride * height nos backends FFI (macOS / Linux) e width * height no Windows (YUY2: width * 2 * height)
  • timestamp_us é em microssegundos, mas a origem varia por backend. Use apenas para diferenças relativas entre quadros, não como hora do relógio
  • pixel_buffer é Some no macOS e None nos demais

Restrições do callback de quadro

O tipo do callback é F: Fn(VideoFrame<'_>) + Send + 'static.

  • As slices de VideoFrame são válidas apenas durante a execução do callback. Se precisar usá-las depois, copie com frame.to_owned() para VideoFrameOwned
  • O callback é executado na thread de captura do backend. Processamento pesado pode causar perda de quadros; use sync_channel + try_send para transferir para outra thread
  • O callback não é FnMut, portanto, para atualizar estado, use Cell / RefCell / Mutex / tipos atômicos (mutabilidade interior). O callback não requer Sync, pois nunca é chamado concorrentemente
  • O callback nunca deve entrar em pânico. No macOS/Linux, o processo pode abortar. No Windows, a thread de captura termina e start() após stop() retorna Error::CaptureFaulted (sem stop() intermediário, start() considera que está executando e retorna Ok(())). A biblioteca não usa catch_unwind para absorver pânicos
  • Não chame stop() nem faça drop de VideoCapture dentro do callback. No Linux (V4L2 / PipeWire) pode causar use-after-free; no macOS, deadlock. No Windows, VideoCapture é !Send, portanto, safe Rust não permite passá-lo ao callback. Para parar, notifique outra thread via canal ou flag, e chame stop() nessa thread

Segurança entre threads

TipoSendSyncObservações
VideoDeviceList / VideoDeviceSimSim
VideoCapture (macOS / Linux)SimNão
VideoCapture (Windows)NãoNãoMantém guarda de inicialização COM; use e faça drop na mesma thread que criou
VideoFrameOwned / PixelBufferSimNãoArc<VideoFrameOwned> não pode ser compartilhado entre threads. Distribua clonando ou encapsulando em Mutex
ErrorSimSimImplementa Copy

Exemplos de código

Enumeração de dispositivos

use shiguredo_video_device::VideoDeviceList;

fn main() -> shiguredo_video_device::Result<()> {
    // Enumera dispositivos com o backend padrão
    let devices = VideoDeviceList::enumerate()?;
    for device in &devices {
        // name() / unique_id() retornam Result porque podem falhar nos backends FFI
        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(())
}

Receber quadros via canal (padrão 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 com limite para contrapressão. Canais ilimitados podem crescer a memória se o receptor for 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<'_>| {
        // As slices são válidas apenas durante o callback; copie com to_owned() antes de enviar
        // Se o receptor estiver cheio, descarta o quadro sem bloquear
        let _ = tx.try_send(frame.to_owned());
    })?;

    capture.start()?;

    for _ in 0..30 {
        let frame = rx.recv_timeout(Duration::from_secs(5))?;
        // Resolução e formato reais estão no quadro (config() retorna valores solicitados)
        println!(
            "{}x{} {} ts={}us",
            frame.width, frame.height, frame.pixel_format, frame.timestamp_us
        );
    }

    // stop() aguarda a conclusão do callback antes de retornar. Não chamar dentro do callback
    capture.stop();
    Ok(())
}

Especificar dispositivo e 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 video device found".into());
    };

    // Se o dispositivo reportar NV12 a 1280x720, solicita esse formato; senão, deixa o backend decidir
    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 {
        // O valor de unique_id() pode ser passado diretamente para 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()),
    }
}

Remover stride para criar buffer contínuo

Útil quando codificadores exigem um buffer contínuo exato.

use shiguredo_video_device::{PixelFormat, VideoFrame};

/// Concatena os bytes válidos de cada linha de um plano com stride
fn pack_plane(src: &[u8], stride: usize, row_bytes: usize, rows: usize) -> Option<Vec<u8>> {
    if row_bytes > stride {
        return None;
    }
    // Dimensões vêm do dispositivo e podem não ser confiáveis; aloca apenas o necessário
    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)
}

/// Empacota o quadro em um buffer contínuo sem stride. Retorna None se as dimensões forem 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 e V intercalados; bytes válidos por linha = 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 contém plano U seguido pelo 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 para 2 pixels
        PixelFormat::Yuy2 => pack_plane(frame.data, stride, chroma_w * 4, h),
        // MJPEG é payload JPEG sem stride
        PixelFormat::Mjpeg => Some(frame.data.to_vec()),
        PixelFormat::Unknown(_) => None,
    }
}

Usar backend explícito (Linux PipeWire)

// Assume que a feature pipewire está ativada nas dependências
// Para evitar unused import em outros SOs, adicione cfg ao use
#[cfg(target_os = "linux")]
use shiguredo_video_device::{VideoCapture, VideoCaptureConfig};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    #[cfg(target_os = "linux")]
    {
        let config = VideoCaptureConfig::default();
        // new_pipewire existe apenas quando a feature pipewire está ativada
        // e o SO alvo é Linux
        let mut capture = VideoCapture::new_pipewire(config, |frame| {
            println!("Quadro capturado via PipeWire: {}x{}", frame.width, frame.height);
        })?;
        capture.start()?;
        std::thread::sleep(std::time::Duration::from_secs(5));
        capture.stop();
    }
    Ok(())
}

Tipo de erro

pub enum Error {
    BackendUnavailable,
    CaptureFailed(String),
    CaptureFaulted,
    DeviceNotFound(String),
    InvalidCaptureConfig(String),
    PermissionDenied(String),
    UnsupportedPixelFormat(PixelFormat),
    Other(String),
}

Descrição dos erros

  • BackendUnavailable: o backend solicitado não está disponível (por exemplo, chamar new_v4l2() sem a feature v4l2 ou em um SO que não seja Linux)
  • CaptureFailed: erro interno do backend ao iniciar ou durante a captura
  • CaptureFaulted: a thread de captura falhou (apenas no Windows, quando o callback entra em pânico ou ocorre uma falha irrecuperável)
  • DeviceNotFound: o dispositivo especificado em device_id não foi encontrado ou está desconectado
  • InvalidCaptureConfig: configuração inválida (por exemplo, dimensões negativas no Windows)
  • PermissionDenied: permissão negada (por exemplo, câmera bloqueada no macOS)
  • UnsupportedPixelFormat: formato de pixel não suportado pelo backend configurado
  • Other: outros erros (descrição em string)

Diferenças de comportamento entre backends

AspectomacOS (AVFoundation)Linux (V4L2)Linux (PipeWire)Windows (Media Foundation)
Enumeração (enumerate())Retorna dispositivos conectados, incluindo câmeras virtuaisRetorna dispositivos V4L2 (/dev/video*) que suportam capturaRetorna nós do PipeWire com fluxo de vídeoRetorna dispositivos de captura de vídeo do Windows
unique_id()Identificador único do dispositivo (pode ser uma string longa)Caminho do dispositivo (ex: /dev/video0)Identificador do nó PipeWireIdentificador do dispositivo (GUID)
Resolução solicitada vs realBackend tenta negociar a resolução mais próxima; pode falhar se não suportarBackend negocia a resolução exata, se disponível; caso contrário, usa a mais próximaBackend negocia com o PipeWire; a resolução real pode diferirBackend negocia; pode redimensionar se necessário
min_fps / max_fpsValores aproximados baseados nas faixas reportadas pelo dispositivoValores exatos do intervalo do formato V4L2Valores aproximados; podem ser imprecisosValores aproximados; podem ser imprecisos
Callback em pânicoPode abortar o processoPode abortar o processoPode abortar o processoThread de captura termina; start() subsequente retorna Error::CaptureFaulted
pixel_bufferSome(PixelBuffer) com referência ao CVPixelBufferNoneNoneNone
Suporte a MJPEGNãoSim (com feature mjpeg)NãoNão
Permissão de câmeraSolicita permissão na primeira chamada a new*() (bloqueante)Não exige permissão especial (depende das permissões de dispositivo)Não exige permissão especialExige permissão via configurações do Windows
COM inicializaçãoN/AN/AN/AInicializa COM no thread de criação; VideoCapture é !Send

Nota: As diferenças acima são baseadas na documentação e no código fonte. Sempre teste seu aplicativo em todas as plataformas alvo.

Considerações finais

  • A biblioteca é projetada para baixa latência e mínimo overhead. Evite alocações desnecessárias dentro do callback
  • Use VideoFrameOwned para transferir quadros entre threads, e Arc<VideoFrameOwned> ou Mutex para compartilhamento
  • Para aplicativos que exigem alto desempenho, considere usar pixel_buffer no macOS para evitar cópias desnecessárias com o VideoToolbox
  • A feature mjpeg (apenas Linux V4L2) requer que o dispositivo suporte MJPEG; a decodificação fica por conta do usuário
  • O suporte a PipeWire ainda é experimental em algumas distribuições Linux; verifique a compatibilidade do seu sistema

Habilidades Relacionadas