Communitygithub.com

shiguredo/shiguredo-video-device

시온기도(時雨堂)의 크로스플랫폼 카메라 캡처 라이브러리 shiguredo_video_device의 기능 및 API 레퍼런스입니다. macOS(AVFoundation), Linux(V4L2/PipeWire), Windows(Media Foundation)에서 장치 열거, 프레임 캡처, feature 기반 백엔드 선택, 픽셀 포맷, 프레임 메모리 레이아웃, 프레임 콜백 제약, 백엔드별 동작 차이에 대한 질문 시 사용합니다.

shiguredo-video-device란 무엇인가요?

`shiguredo_video_device`는 macOS, Linux, Windows에서 카메라 장치 열거와 프레임 캡처를 공통 Rust API로 제공하는 라이브러리입니다. `VideoDeviceList`와 `VideoCapture` 타입을 통해 플랫폼별 차이를 추상화하며, AVFoundation, V4L2, PipeWire, Media Foundation 백엔드를 Cargo feature로 선택할 수 있습니다. NV12, YUY2, I420 픽셀 포맷과 Linux V4L2에서 MJPEG 패스스루를 지원하며, macOS에서는 추가 복사 없이 VideoToolbox 등에 전달할 수 있는 CVPixelBuffer 참조를 제공합니다. 콜백 기반 캡처는 백엔드 스레드에서 호출되며, 프레임 데이터는 콜백 내에서만 유효하므로 장기 사용 시 `to_owned()`로 소유권을 복사해야 합니다. 또한, FFI 백엔드에서의 스트라이드, stride_uv 처리, 플랫폼별 스레드 안전성, 콜백 내 금지 동작 등에 대한 명확한 가이드를 제공하여 안정적인 멀티플랫폼 비디오 캡처 애플리케이션 개발을 지원합니다.

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

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

shiguredo_video_device

macOS / Linux / Windows의 카메라 열거와 프레임 캡처를 플랫폼 공통 Rust API로 제공하는 라이브러리입니다.

특징

  • 크로스플랫폼: macOS (AVFoundation), Linux (V4L2 / PipeWire), Windows (Media Foundation)을 동일한 API로 다룹니다.
  • feature 기반 백엔드 선택: 활성화할 백엔드와 기본 백엔드를 feature로 선택합니다.
  • 콜백 기반: 캡처 중 백엔드 스레드에서 프레임마다 콜백이 호출됩니다.
  • 픽셀 포맷: NV12 / YUY2 / I420, Linux (V4L2)에서는 MJPEG 패스스루 (mjpeg feature)를 지원합니다.
  • 적은 의존성: Rust 의존 크레이트는 Windows (mf feature)의 windows 크레이트뿐입니다.
  • CVPixelBuffer 참조 (macOS): 프레임에 CVPixelBuffer 참조 (PixelBuffer)가 포함되어, 복사 없이 VideoToolbox 등에 전달할 수 있습니다.

버전 정보

  • crate 이름: shiguredo_video_device
  • 버전: 2026.4.0
  • Rust Edition: 2024
  • 최소 Rust 버전: 1.95
  • 최소 macOS 버전: 14.0 (AVCaptureDeviceTypeExternal을 참조하므로 macOS 13 이하에서는 동작하지 않습니다.)
  • 라이선스: Apache-2.0
  • 리포지토리: https://github.com/shiguredo/video-device-rs

Error / PixelFormat에는 #[non_exhaustive]가 붙어 있지 않으므로 match로 모든 배리언트를 열거할 수 있습니다.

도입

[dependencies]
shiguredo_video_device = "2026.4"

기본적으로 macOS는 AVFoundation, Linux는 V4L2, Windows는 Media Foundation을 기본 백엔드로 사용합니다. 빌드 대상 OS 외의 feature는 무시되므로, OS별로 의존성을 나눌 필요가 없습니다.

feature

feature대상 OS설명
avfmacOSAVFoundation 백엔드를 활성화합니다.
v4l2LinuxV4L2 백엔드를 활성화합니다.
pipewireLinuxPipeWire 백엔드를 활성화합니다. (libpipewire-0.3에 동적 링크)
mfWindowsMedia Foundation 백엔드를 활성화합니다. (windows 크레이트에 의존)
mjpegLinuxV4L2에서 MJPEG 패스스루 캡처를 활성화합니다. (v4l2 포함)
default-avfmacOS기본 백엔드를 AVFoundation으로 설정합니다. (avf 포함)
default-v4l2Linux기본 백엔드를 V4L2로 설정합니다. (v4l2 포함)
default-pipewireLinux기본 백엔드를 PipeWire로 설정합니다. (pipewire 포함)
default-mfWindows기본 백엔드를 Media Foundation으로 설정합니다. (mf 포함)

기본 feature는 default-avf / default-v4l2 / default-mf입니다.

  • 동일 OS에서 default-*를 2개 이상 활성화하면 빌드 스크립트가 패닉하여 빌드할 수 없습니다.
  • Linux에서 기본값을 PipeWire로 설정하려면 default-features = false가 필요합니다. 이 경우 macOS / Windows의 기본값도 해제되므로, 크로스플랫폼 애플리케이션에서는 default-avf / default-mf를 다시 지정해야 합니다.
  • default-*가 없는 OS에서는 VideoDeviceList::enumerate() / VideoCapture::new()가 존재하지 않습니다. enumerate_avf() / new_v4l2() 등의 명시적 API를 사용해야 합니다.
  • 백엔드가 하나도 활성화되지 않은 OS에서는 VideoCapture / VideoDevice / VideoDeviceList가 공개되지 않습니다. (Error / PixelFormat 등의 타입만 남습니다.)
  • Linux에서는 v4l2와 pipewire를 동시에 활성화할 수 있습니다.
  • 크레이트 내부의 enable_* cfg는 빌드 스크립트가 이 크레이트에만 설정하는 것으로, 사용 측에서는 참조할 수 없습니다. 사용 측은 #[cfg(target_os = "linux")]와 자체 feature로 분기해야 합니다.

Linux 기본값을 PipeWire로 설정하고, macOS / Windows는 기본값을 그대로 사용하는 경우:

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

기본값은 V4L2로 유지하고 PipeWire와 MJPEG도 사용할 수 있도록 하는 경우:

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

빌드 요구 사항

OS요구 사항
macOSXcode Command Line Tools (bindgen용 libclang 포함)
Linux (V4L2)libclang (Ubuntu의 경우 libclang-dev)
Linux (PipeWire)libclang과 libpipewire-0.3-dev, pkg-config
Windows추가 의존 패키지 불필요

코어 API

공개 타입

타입설명주요 API
VideoDeviceList장치 열거 결과enumerate(), enumerate_avf(), enumerate_v4l2(), enumerate_pipewire(), enumerate_mf(), as_slice(), len(), is_empty(), &VideoDeviceList: IntoIterator
VideoDevice장치 1대name() -> Result<String>, unique_id() -> Result<String>, formats() -> Vec<VideoFormat>, format_count() -> usize
VideoFormat장치가 보고하는 포맷width: i32, height: i32, min_fps: f32, max_fps: f32, pixel_format: PixelFormat
VideoCapture캡처 세션new(), new_avf(), new_v4l2(), new_pipewire(), new_mf(), start(), stop(), config()
VideoCaptureConfig캡처 설정 (Default 구현)device_id: Option<String>, width: i32, height: i32, fps: i32, pixel_format: Option<PixelFormat>
VideoFrame<'a>콜백에 전달되는 빌린 프레임data, uv_data, width, height, stride, stride_uv, pixel_format, timestamp_us, pixel_buffer, to_owned()
VideoFrameOwned소유 프레임VideoFrame과 동일한 필드 (Vec<u8>로 보관), as_frame()
PixelFormat픽셀 포맷Nv12, Yuy2, I420, Mjpeg, Unknown(u32), name(), to_raw(), Display
PixelBuffermacOS CVPixelBuffer 참조as_ptr() -> *mut c_void (Clone 시 CFRetain, Drop 시 CFRelease)
Error / Result<T>에러 타입과 결과 타입"에러 타입" 절 참조

enumerate() / new()는 기본 백엔드(default-*)를 사용합니다. enumerate_*() / new_*()는 빌드 대상 OS가 일치하고 해당 feature가 활성화된 경우에만 존재합니다.

VideoDeviceList / VideoDevice

  • VideoDevice는 VideoDeviceList에서 빌린 참조 (&VideoDevice)로만 가져올 수 있습니다. 리스트를 버린 후에도 정보를 사용해야 한다면 unique_id() 등의 값을 복사해 두세요.
  • unique_id() 값은 그대로 VideoCaptureConfig::device_id에 전달할 수 있습니다. (동일한 백엔드인 경우에 한정)
  • name() / unique_id()는 FFI 백엔드(macOS / Linux)에서 C 측이 NULL 또는 비 UTF-8을 반환할 수 있으므로 Result를 반환합니다. Windows에서는 실패하지 않습니다.
  • format_count()는 C 측이 보고하는 엔트리 수로, 획득하지 못한 엔트리를 제외한 formats().len()보다 클 수 있습니다.
  • VideoFormat의 min_fps / max_fps는 백엔드 의존적인 대략적인 값입니다. ("백엔드별 동작 차이" 절 참조)

VideoCapture 라이프사이클

  • new*()는 세션만 생성하며 캡처는 시작되지 않습니다. (macOS는 권한이 미결정된 경우 OS 대화상자를 표시하고 응답까지 반환하지 않습니다.)
  • start()는 멱등적이며 실행 중이면 Ok(())를 반환합니다. stop() 후의 재 start()도 허용됩니다.
  • stop()은 블로킹하며 캡처 스레드와 콜백의 완료를 기다린 후 반환합니다. 실행 중이 아니면 아무것도 하지 않습니다.
  • drop 시에는 실행 중이면 stop()과 마찬가지로 블로킹 정지 후 세션을 파기합니다.
  • config()는 new*()에 전달된 요청 값을 반환합니다. 협상 후의 실제 값이 아니므로, 해상도 · 포맷 · 스트라이드는 각 프레임의 필드를 참조해야 합니다.

VideoCaptureConfig

필드Default 값설명
device_idNoneVideoDevice::unique_id() 값. None은 기본 장치를 의미합니다.
width640요청하는 너비
height480요청하는 높이
fps30요청하는 프레임 레이트
pixel_formatNone요청하는 픽셀 포맷. None은 백엔드가 선택합니다.
  • Windows에서는 width / height / fps가 0 이하이면 Error::InvalidCaptureConfig를 반환합니다. 다른 백엔드는 Rust 측에서 검증하지 않습니다.
  • pixel_format: Some(PixelFormat::Unknown(_))는 모든 백엔드에서 Error::UnsupportedPixelFormat을 반환합니다.
  • pixel_format: Some(PixelFormat::Mjpeg)는 Linux V4L2 + mjpeg feature에서만 사용할 수 있습니다. 그 외에는 Error::UnsupportedPixelFormat(PixelFormat::Mjpeg)를 반환합니다.
  • 요청 값은 어디까지나 희망 사항이며, 실제 값은 백엔드와 장치가 결정합니다.

PixelFormat

배리언트내용to_raw() (FourCC)
Nv12YUV 4:2:0 세미플레너 (Y 플레인 + UV 인터리브 플레인)0x3231564E (NV12)
Yuy2YUV 4:2:2 팩 (Y0 U Y1 V)0x32595559 (YUY2)
I420YUV 4:2:0 플레너 (Y / U / V 3플레인)0x30323449 (I420)
MjpegMotion JPEG. 디코딩은 사용자 책임0x47504A4D (MJPG)
Unknown(u32)알 수 없는 FourCC보관 중인 값
  • name()은 "NV12" / "YUY2" / "I420" / "MJPEG" / "Unknown"을 반환합니다. Display는 Unknown인 경우 Unknown(0x????????) 형식으로 값을 포함합니다.
  • 현재 백엔드는 Unknown을 생성하지 않습니다. 알 수 없는 포맷은 formats()에서 제외되며, 해당 프레임은 콜백에 전달되지 않습니다.

프레임 메모리 레이아웃

포맷datauv_datastridestride_uv
NV12Y 플레인Some: UV 인터리브 플레인 (ceil(height / 2) 행)Y 1행의 바이트 수UV 1행의 바이트 수
I420Y 플레인Some: U 플레인 바로 뒤에 V 플레인이 이어지는 연결 영역 (각 stride_uv * ceil(height / 2) 바이트)Y 1행의 바이트 수U / V 각각 1행의 바이트 수
YUY2팩 데이터None1행의 바이트 수0
MJPEGJPEG 페이로드 (길이는 data.len())None00
  • stride는 행 끝의 패딩을 포함할 수 있습니다. 너비만큼의 연속 버퍼가 필요하면 행마다 재정렬해야 합니다. ("코드 예"의 pack_frame 참조)
  • NV12 / I420 / YUY2의 data.len()은 FFI 백엔드(macOS / Linux)에서는 stride * height, Windows에서는 width * height (YUY2는 width * 2 * height)입니다.
  • timestamp_us는 마이크로초 단위이지만 기준점은 백엔드마다 다릅니다. 프레임 간의 상대 시간으로만 사용하고 벽시계 시간으로는 사용하지 마세요.
  • pixel_buffer는 macOS에서는 Some, 그 외에서는 None입니다.

프레임 콜백 제약

콜백의 타입은 F: Fn(VideoFrame<'_>) + Send + 'static입니다.

  • VideoFrame의 슬라이스는 콜백 실행 중에만 유효합니다. 콜백 이후에도 사용하려면 frame.to_owned()로 VideoFrameOwned에 복사해야 합니다.
  • 콜백은 백엔드의 캡처 스레드에서 실행됩니다. 무거운 처리를 하면 프레임이 드롭될 수 있으므로, sync_channel + try_send 등을 사용하여 별도 스레드에 전달하세요.
  • FnMut가 아니므로 상태 업데이트에는 Cell / RefCell / Mutex / 원자형 등의 내부 가변성을 사용해야 합니다. Sync를 요구하지 않으므로 콜백이 병렬로 호출되지는 않습니다.
  • 콜백에서 패닉을 일으키면 안 됩니다. macOS / Linux에서는 프로세스가 중단될 수 있습니다. Windows에서는 캡처 스레드가 종료되고, stop() 이후의 start()가 Error::CaptureFaulted를 반환합니다. (stop()을 거치지 않는 start()는 실행 중인 것으로 간주하여 Ok(())를 반환합니다.) 라이브러리는 catch_unwind로 흡수하지 않습니다.
  • 콜백 내에서 stop()을 호출하거나 VideoCapture를 drop해서는 안 됩니다. Linux (V4L2 / PipeWire)에서는 use-after-free, macOS에서는 데드락이 발생할 수 있습니다. Windows의 VideoCapture는 !Send이므로 safe Rust에서는 콜백에 전달할 수 없습니다. 정지는 채널이나 플래그로 별도 스레드에 알리고, 해당 스레드에서 stop()을 호출하세요.

스레드 안전성

타입SendSync비고
VideoDeviceList / VideoDeviceYesYes
VideoCapture (macOS / Linux)YesNo
VideoCapture (Windows)NoNoCOM 초기화 가드를 보유하므로, 생성한 스레드에서 사용하고 해당 스레드에서 drop해야 합니다.
VideoFrameOwned / PixelBufferYesNoArc<VideoFrameOwned>는 스레드 간에 공유할 수 없습니다. 여러 수신 측에 배포하려면 clone하거나 Mutex로 감싸야 합니다.
ErrorYesYesCopy

코드 예

장치 열거

use shiguredo_video_device::VideoDeviceList;

fn main() -> shiguredo_video_device::Result<()> {
    // 기본 백엔드로 장치 열거
    let devices = VideoDeviceList::enumerate()?;
    for device in &devices {
        // name() / unique_id()는 FFI 백엔드에서 실패할 수 있으므로 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(())
}

채널을 통해 프레임 수신 (권장 패턴)

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>> {
    // 상한이 있는 채널로 역압을 가함. 무제한 채널은 수신 측이 느리면 메모리가 계속 증가함
    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<'_>| {
        // 슬라이스는 콜백 중에만 유효하므로 to_owned()로 복사 후 전송
        // 수신 측이 막혀 있으면 블로킹하지 않고 프레임을 버림
        let _ = tx.try_send(frame.to_owned());
    })?;

    capture.start()?;

    for _ in 0..30 {
        let frame = rx.recv_timeout(Duration::from_secs(5))?;
        // 실제 해상도와 포맷은 프레임 쪽을 확인 (config()는 요청 값)
        println!(
            "{}x{} {} ts={}us",
            frame.width, frame.height, frame.pixel_format, frame.timestamp_us
        );
    }

    // stop()은 콜백 완료를 기다린 후 반환. 콜백 내에서 호출하지 말 것
    capture.stop();
    Ok(())
}

장치와 포맷 지정

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());
    };

    // 장치가 1280x720의 NV12를 보고하면 요청하고, 없으면 백엔드에 맡김
    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 {
        // unique_id() 값은 그대로 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()),
    }
}

stride를 제거하여 연속 버퍼로 정리

인코더 등이 너비만큼의 연속 버퍼를 요구하는 경우에 사용합니다.

use shiguredo_video_device::{PixelFormat, VideoFrame};

/// stride가 있는 플레인에서 각 행의 유효 바이트만 연결
fn pack_plane(src: &[u8], stride: usize, row_bytes: usize, rows: usize) -> Option<Vec<u8>> {
    if row_bytes > stride {
        return None;
    }
    // 크기는 장치에서 비롯된 것으로 신뢰할 수 없으므로 미리 할당하지 않고 실제 데이터만큼 확장
    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)
}

/// 프레임을 stride 없는 연속 버퍼로 정리. 크기가 불일치하면 None 반환
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와 V가 번갈아 배열되므로 1행의 유효 바이트 수는 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는 U 플레인 바로 뒤에 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픽셀에 4바이트
        PixelFormat::Yuy2 => pack_plane(frame.data, stride, chroma_w * 4, h),
        // MJPEG는 stride가 없는 JPEG 페이로드
        PixelFormat::Mjpeg => Some(frame.data.to_vec()),
        PixelFormat::Unknown(_) => None,
    }
}

기본 외 백엔드 명시 (Linux PipeWire)

// 의존성 features에서 pipewire를 활성화한 상태를 가정
// 다른 OS에서 unused import가 발생하지 않도록 use에도 cfg 추가
#[cfg(target_os = "linux")]
use shiguredo_video_device::VideoCapture;

#[cfg(target_os = "linux")]
fn open_pipewire() -> Result<VideoCapture, Box<dyn std::error::Error>> {
    use shiguredo_video_device::VideoCaptureConfig;
    let config = VideoCaptureConfig::default();
    let capture = VideoCapture::new_pipewire(config, |frame| {
        println!("Frame {}x{}", frame.width, frame.height);
    })?;
    Ok(capture)
}

fn main() {
    #[cfg(target_os = "linux")]
    {
        if let Ok(capture) = open_pipewire() {
            // ... capture.start() 등
        }
    }
}

에러 타입

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Error {
    /// 장치를 찾을 수 없음
    DeviceNotFound,
    /// 장치 ID가 유효하지 않음
    InvalidDeviceId,
    /// 캡처 설정이 유효하지 않음 (Windows: width/height/fps가 0 이하)
    InvalidCaptureConfig,
    /// 지정한 픽셀 포맷을 지원하지 않음
    UnsupportedPixelFormat(PixelFormat),
    /// 캡처 시작 실패 (카메라가 사용 중인 경우 등)
    StartFailed,
    /// 캡처 중 오류 발생 (백엔드 의존)
    CaptureFaulted,
    /// FFI 호출 에러 (macOS / Linux의 AVFoundation / V4L2 / PipeWire의 내부 에러)
    BackendError(i32),
    /// FFI 호출 에러 (Windows의 HRESULT)
    BackendErrorHr(i32),
    /// 캡처 중단 (Windows의 MF_E_SHUTDOWN 등)
    BackendShutdown,
    /// 권한 부족 또는 거부됨
    PermissionDenied,
    /// 알 수 없는 백엔드 ID
    UnknownBackend,
}

백엔드별 동작 차이

macOS (AVFoundation)

  • VideoDevice::name() / unique_id()는 AVCaptureDevice.uniqueID / localizedName을 반환합니다. SessionPreset을 사용하지 않고 AVCaptureDeviceInput + AVCaptureVideoDataOutput + pixelBufferAttributes로 캡처를 구성합니다.
  • VideoFormat.min_fps / max_fps는 AVFrameRateRange의 minFrameRate / maxFrameRate를 나타냅니다.
  • new*() 시 권한이 미결정이면 OS 대화상자가 표시되고, 승인될 때까지 차단됩니다.
  • VideoFrame.timestamp_us는 CMSampleBufferGetOutputPresentationTimeStamp의 value / timescale * 1_000_000입니다.
  • frame.pixel_buffer는 CMSampleBufferGetImageBuffer에서 얻은 CVPixelBuffer의 참조입니다. VideoFrame이 Drop되면 CFRelease됩니다. VideoToolbox 등에 전달하려면 as_ptr()를 사용하세요.

Linux V4L2

  • VideoDevice::name() / unique_id()는 VIDIOC_QUERYCAP의 card / bus_info를 반환합니다. bus_info가 없는 경우 driver 필드로 대체됩니다.
  • VideoFormat.min_fps / max_fps는 VIDIOC_ENUM_FRAMEINTERVALS에서 얻습니다. 불연속(STEPWISE)인 경우 첫 번째와 마지막 값입니다. 연속(DISCRETE)인 경우 모든 간격의 최소/최대입니다. 얻을 수 없는 경우 각각 0.0 / 0.0입니다.
  • V4L2의 픽셀 포맷 변환은 V4L2_PIX_FMT_NV12, V4L2_PIX_FMT_YUYV, V4L2_PIX_FMT_YUV420, V4L2_PIX_FMT_MJPEG에 매핑됩니다.
  • 캡처는 mmap을 사용한 스트리밍 I/O입니다. VIDIOC_REQBUFS(count=4), VIDIOC_QBUF, VIDIOC_STREAMON 후 poll로 프레임을 수신합니다.
  • MJPEG 포맷을 사용하려면 mjpeg feature가 필요합니다. 그 외의 경우 MJPEG를 요청하면 Error::UnsupportedPixelFormat이 반환됩니다.
  • mjpeg feature가 활성화되지 않은 상태에서 장치가 MJPEG만 지원하는 경우, VideoDevice::formats()에는 포맷이 포함되지 않습니다.
  • 프레임의 timestamp_us는 v4l2_buffer.timestamp.tv_sec * 1_000_000 + tv_usec입니다.

Linux PipeWire

  • VideoDevice::name() / unique_id()는 SPA_PROP_*.nodeName / SPA_PROP_*.objectSerial 또는 SPA_PROP_*.objectPath를 반환합니다 (우선순위 순서).
  • PipeWire 백엔드는 libpipewire-0.3에 동적 링크됩니다. 시스템에 라이브러리와 데몬이 설치되어 있어야 합니다.
  • PipeWire는 카메라를 PW_DIRECTION_INPUT 포트로 감지합니다. SPA 노드와 포트의 property를 사용하여 장치 목록을 작성합니다.
  • VideoFormat.min_fps / max_fps는 SPA 포트 및 VideoParam의 VideoInfo에서 추출합니다. min_fps는 VideoInfo.min_fps, max_fps는 VideoInfo.max_fps 변환 값입니다. 정보를 얻을 수 없는 경우 각각 0.0 / 0.0입니다.
  • 픽셀 포맷 변환: SPA_VIDEO_FORMAT_NV12 -> Nv12, SPA_VIDEO_FORMAT_YUY2 -> Yuy2, SPA_VIDEO_FORMAT_I420 -> I420. MJPEG는 지원되지 않습니다.
  • 캡처는 PipeWire 스트림(pw_stream)을 사용하며 PW_ID_ANY로 설정됩니다. 콜백은 pw_stream의 프로세스 콜백 내에서 최신 버퍼를 복사하여 프레임으로 만듭니다.
  • 프레임의 timestamp_us는 pw_time.now를 기반으로 한 상대 시간입니다.
  • 수집된 포맷 정보가 없거나 SPA node 정보가 필요하지만 얻을 수 없는 경우, 빈 배열 대신 Error::DeviceNotFound가 발생할 수 있습니다.

Windows (Media Foundation)

  • VideoDevice::name() / unique_id()는 IMFActivate의 MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME / MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK를 반환합니다. 두 속성은 항상 존재하므로 Result::Ok만 반환합니다 (절대 실패하지 않습니다).
  • VideoFormat.min_fps / max_fps는 MF_MT_FRAME_RATE_RANGE에서 얻습니다. 얻을 수 없는 경우 각각 0.0 / 0.0입니다. Min/Max가 반전되어 보고되는 경우 max가 min보다 작거나 같으면 (min_fps, max_fps) = (max_fps, min_fps)로 교체됩니다.
  • 캡처는 MFCreateSourceReaderFromMediaSource를 사용하며, IMFMediaSource 내부의 IMFMediaType에서 지원하는 미디어 타입 중 첫 번째를 사용합니다. 설정과 일치하지 않는 경우 첫 번째 유효한 타입을 사용합니다.
  • VideoFrame.timestamp_us는 IMFSample의 타임스탬프 MFSampleExtension_Discontinuity를 고려하며, MFTIME을 100ns 단위에서 마이크로초로 변환합니다.
  • VideoCapture는 !Send이므로 생성한 스레드에서 사용하고 drop해야 합니다.

트러블슈팅

"default-*"를 2개 이상 활성화했을 때 빌드 에러

error: Cannot enable multiple default backends for the same OS

예: Linux에서 default-v4l2와 default-pipewire를 동시에 활성화한 경우. 중복을 제거하십시오.

macOS에서 캡처가 시작되지 않음 (Error::PermissionDenied)

  • 시스템 환경설정 > 개인정보 보호 및 보안 > 카메라에서 애플리케이션 권한을 확인하십시오.
  • 시뮬레이터는 카메라에 액세스할 수 없습니다.

Linux에서 캡처 시작 실패 (Error::StartFailed)

  • 권한 확인: /dev/video*의 그룹(video)에 사용자가 속해 있는지 확인 (groups $USER).
  • V4L2의 경우 v4l2-ctl --list-devices로 장치가 표시되는지 확인.
  • PipeWire의 경우 pw-top 또는 pw-cli list-objects로 카메라 노드가 표시되는지 확인.

Linux에서 MJPEG 캡처가 안 됨

  • 의존성에 features = ["mjpeg", "v4l2"]가 포함되어 있는지 확인하십시오.
  • 장치가 MJPG 포맷을 지원하는지 확인하려면 v4l2-ctl --list-formats-ext를 실행하십시오.

Windows에서 캡처 시작 실패 (Error::CaptureFaulted)

  • 다른 애플리케이션이 카메라를 사용하고 있지 않은지 확인하십시오.
  • 카메라 드라이버를 업데이트하십시오.

콜백이 호출되지 않음

  • VideoCaptureConfig의 device_id가 유효한지 확인하십시오. (VideoDevice::enumerate() 결과와 비교)
  • 요청한 해상도/포맷이 장치에서 지원되는지 확인하십시오. 장치가 지원하지 않는 설정은 무시될 수 있습니다.
  • start()가 호출되었는지 확인하십시오. (new*()는 캡처를 시작하지 않습니다.)

프레임 드롭이 발생함

  • 콜백 내에서 무거운 처리를 하지 않는지 확인하십시오. 채널 등을 통해 별도 스레드로 프레임을 전달하십시오.
  • sync_channel의 버퍼 크기를 늘리거나, 필요에 따라 프레임을 선택적으로 처리하십시오.

추가 정보

더 자세한 내용은 리포지토리의 README 및 예제를 참조하십시오.

관련 스킬