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 featuremf) 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
| feature | SO alvo | Descrição |
|---|---|---|
avf | macOS | Ativa o backend AVFoundation |
v4l2 | Linux | Ativa o backend V4L2 |
pipewire | Linux | Ativa o backend PipeWire (link dinâmico com libpipewire-0.3) |
mf | Windows | Ativa o backend Media Foundation (depende da crate windows) |
mjpeg | Linux | Ativa a captura com passagem MJPEG no V4L2 (inclui v4l2) |
default-avf | macOS | Define AVFoundation como backend padrão (inclui avf) |
default-v4l2 | Linux | Define V4L2 como backend padrão (inclui v4l2) |
default-pipewire | Linux | Define PipeWire como backend padrão (inclui pipewire) |
default-mf | Windows | Define 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 reespecificardefault-avf/default-mf - Em SOs sem nenhum
default-*,VideoDeviceList::enumerate()eVideoCapture::new()não existem. Use as APIs explícitas comoenumerate_avf()/new_v4l2() - Se nenhum backend estiver ativado para um SO,
VideoCapture,VideoDeviceeVideoDeviceListnão serão exportados (apenas tipos comoError/PixelFormatpermanecerão) - No Linux,
v4l2epipewirepodem 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
| SO | Requisitos |
|---|---|
| macOS | Xcode Command Line Tools (inclui libclang para bindgen) |
| Linux (V4L2) | libclang (no Ubuntu: libclang-dev) |
| Linux (PipeWire) | libclang, libpipewire-0.3-dev e pkg-config |
| Windows | Nenhum pacote adicional necessário |
API principal
Tipos exportados
| Tipo | Descrição | API principal |
|---|---|---|
VideoDeviceList | Resultado da enumeração de dispositivos | enumerate(), enumerate_avf(), enumerate_v4l2(), enumerate_pipewire(), enumerate_mf(), as_slice(), len(), is_empty(), &VideoDeviceList: IntoIterator |
VideoDevice | Um dispositivo | name() -> Result<String>, unique_id() -> Result<String>, formats() -> Vec<VideoFormat>, format_count() -> usize |
VideoFormat | Formato relatado por um dispositivo | width: i32, height: i32, min_fps: f32, max_fps: f32, pixel_format: PixelFormat |
VideoCapture | Sessão de captura | new(), new_avf(), new_v4l2(), new_pipewire(), new_mf(), start(), stop(), config() |
VideoCaptureConfig | Configuraçã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 callback | data, uv_data, width, height, stride, stride_uv, pixel_format, timestamp_us, pixel_buffer, to_owned() |
VideoFrameOwned | Quadro proprietário | Mesmos campos que VideoFrame (armazenado como Vec<u8>), as_frame() |
PixelFormat | Formato de pixel | Nv12, Yuy2, I420, Mjpeg, Unknown(u32), name(), to_raw(), Display |
PixelBuffer | Referência ao CVPixelBuffer no macOS | as_ptr() -> *mut c_void (Clone faz CFRetain, Drop faz CFRelease) |
Error / Result<T> | Tipo de erro e resultado | Consulte 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
VideoDevicesó pode ser obtido como referência emprestada (&VideoDevice) a partir deVideoDeviceList. Se precisar das informações após descartar a lista, copie valores comounique_id()- O valor de
unique_id()pode ser passado diretamente paraVideoCaptureConfig::device_id(apenas para o mesmo backend) name()/unique_id()retornamResultporque os backends FFI (macOS / Linux) podem retornar NULL ou strings não UTF-8 do lado C. No Windows, nunca falhamformat_count()é o número de entradas relatadas pelo lado C, que pode ser maior queformats().len()(que exclui entradas não obtidas)min_fps/max_fpsdeVideoFormatsã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, retornaOk(()). Também é permitido chamarstart()novamente apósstop()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
dropda sessão, se estiver em execução, realiza uma parada bloqueante igual astop()e depois destrói a sessão config()retorna os valores solicitados emnew*(), não os valores reais após negociação. Para obter resolução, formato e stride reais, consulte os campos de cada quadro
VideoCaptureConfig
| Campo | Valor padrão | Descrição |
|---|---|---|
device_id | None | Valor de VideoDevice::unique_id(). None indica dispositivo padrão |
width | 640 | Largura solicitada |
height | 480 | Altura solicitada |
fps | 30 | Taxa de quadros solicitada |
pixel_format | None | Formato de pixel solicitado. None delega a escolha ao backend |
- No Windows, se
width/height/fpsforem <= 0, retornaError::InvalidCaptureConfig. Os outros backends não validam no lado Rust pixel_format: Some(PixelFormat::Unknown(_))retornaError::UnsupportedPixelFormatem todos os backendspixel_format: Some(PixelFormat::Mjpeg)só é válido no Linux V4L2 com a featuremjpeg. Caso contrário, retornaError::UnsupportedPixelFormat(PixelFormat::Mjpeg)- Os valores solicitados são apenas desejos; os valores reais são determinados pelo backend e dispositivo
PixelFormat
| Variante | Conteúdo | to_raw() (FourCC) |
|---|---|---|
Nv12 | YUV 4:2:0 semi-planar (plano Y + plano UV intercalado) | 0x3231564E (NV12) |
Yuy2 | YUV 4:2:2 empacotado (Y0 U Y1 V) | 0x32595559 (YUY2) |
I420 | YUV 4:2:0 planar (3 planos: Y / U / V) | 0x30323449 (I420) |
Mjpeg | Motion JPEG. Decodificação é responsabilidade do usuário | 0x47504A4D (MJPG) |
Unknown(u32) | FourCC desconhecido | Valor armazenado |
name()retorna"NV12"/"YUY2"/"I420"/"MJPEG"/"Unknown".Displayinclui o valor no formatoUnknown(0x????????)quandoUnknown- Atualmente, nenhum backend gera
Unknown. Formatos desconhecidos são excluídos deformats()e seus quadros nunca são passados ao callback
Layout de memória dos quadros
| Formato | data | uv_data | stride | stride_uv |
|---|---|---|---|---|
| NV12 | Plano Y | Some: plano UV intercalado (altura ceil(height / 2) linhas) | Bytes por linha do Y | Bytes por linha do UV |
| I420 | Plano Y | Some: região contígua com plano U seguido pelo plano V (cada um com stride_uv * ceil(height / 2) bytes) | Bytes por linha do Y | Bytes por linha de cada U / V |
| YUY2 | Dados empacotados | None | Bytes por linha | 0 |
| MJPEG | Payload JPEG (tamanho é data.len()) | None | 0 | 0 |
stridepode incluir padding no final da linha. Se precisar de um buffer contínuo exato, remonte linha por linha (consultepack_frameem "Exemplos de código")- Em NV12 / I420 / YUY2,
data.len()éstride * heightnos backends FFI (macOS / Linux) ewidth * heightno 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ógiopixel_bufferéSomeno macOS eNonenos demais
Restrições do callback de quadro
O tipo do callback é F: Fn(VideoFrame<'_>) + Send + 'static.
- As slices de
VideoFramesão válidas apenas durante a execução do callback. Se precisar usá-las depois, copie comframe.to_owned()paraVideoFrameOwned - O callback é executado na thread de captura do backend. Processamento pesado pode causar perda de quadros; use
sync_channel+try_sendpara transferir para outra thread - O callback não é
FnMut, portanto, para atualizar estado, useCell/RefCell/Mutex/ tipos atômicos (mutabilidade interior). O callback não requerSync, 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ósstop()retornaError::CaptureFaulted(semstop()intermediário,start()considera que está executando e retornaOk(())). A biblioteca não usacatch_unwindpara absorver pânicos - Não chame
stop()nem faça drop deVideoCapturedentro 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 chamestop()nessa thread
Segurança entre threads
| Tipo | Send | Sync | Observações |
|---|---|---|---|
VideoDeviceList / VideoDevice | Sim | Sim | |
VideoCapture (macOS / Linux) | Sim | Não | |
VideoCapture (Windows) | Não | Não | Mantém guarda de inicialização COM; use e faça drop na mesma thread que criou |
VideoFrameOwned / PixelBuffer | Sim | Não | Arc<VideoFrameOwned> não pode ser compartilhado entre threads. Distribua clonando ou encapsulando em Mutex |
Error | Sim | Sim | Implementa 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, chamarnew_v4l2()sem a featurev4l2ou em um SO que não seja Linux)CaptureFailed: erro interno do backend ao iniciar ou durante a capturaCaptureFaulted: 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 emdevice_idnão foi encontrado ou está desconectadoInvalidCaptureConfig: 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 configuradoOther: outros erros (descrição em string)
Diferenças de comportamento entre backends
| Aspecto | macOS (AVFoundation) | Linux (V4L2) | Linux (PipeWire) | Windows (Media Foundation) |
|---|---|---|---|---|
Enumeração (enumerate()) | Retorna dispositivos conectados, incluindo câmeras virtuais | Retorna dispositivos V4L2 (/dev/video*) que suportam captura | Retorna nós do PipeWire com fluxo de vídeo | Retorna 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ó PipeWire | Identificador do dispositivo (GUID) |
| Resolução solicitada vs real | Backend tenta negociar a resolução mais próxima; pode falhar se não suportar | Backend negocia a resolução exata, se disponível; caso contrário, usa a mais próxima | Backend negocia com o PipeWire; a resolução real pode diferir | Backend negocia; pode redimensionar se necessário |
min_fps / max_fps | Valores aproximados baseados nas faixas reportadas pelo dispositivo | Valores exatos do intervalo do formato V4L2 | Valores aproximados; podem ser imprecisos | Valores aproximados; podem ser imprecisos |
| Callback em pânico | Pode abortar o processo | Pode abortar o processo | Pode abortar o processo | Thread de captura termina; start() subsequente retorna Error::CaptureFaulted |
pixel_buffer | Some(PixelBuffer) com referência ao CVPixelBuffer | None | None | None |
| Suporte a MJPEG | Não | Sim (com feature mjpeg) | Não | Não |
| Permissão de câmera | Solicita 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 especial | Exige permissão via configurações do Windows |
| COM inicialização | N/A | N/A | N/A | Inicializa 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
VideoFrameOwnedpara transferir quadros entre threads, eArc<VideoFrameOwned>ouMutexpara compartilhamento - Para aplicativos que exigem alto desempenho, considere usar
pixel_bufferno 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