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 패스스루 (
mjpegfeature)를 지원합니다. - 적은 의존성: Rust 의존 크레이트는 Windows (
mffeature)의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 | 설명 |
|---|---|---|
avf | macOS | AVFoundation 백엔드를 활성화합니다. |
v4l2 | Linux | V4L2 백엔드를 활성화합니다. |
pipewire | Linux | PipeWire 백엔드를 활성화합니다. (libpipewire-0.3에 동적 링크) |
mf | Windows | Media Foundation 백엔드를 활성화합니다. (windows 크레이트에 의존) |
mjpeg | Linux | V4L2에서 MJPEG 패스스루 캡처를 활성화합니다. (v4l2 포함) |
default-avf | macOS | 기본 백엔드를 AVFoundation으로 설정합니다. (avf 포함) |
default-v4l2 | Linux | 기본 백엔드를 V4L2로 설정합니다. (v4l2 포함) |
default-pipewire | Linux | 기본 백엔드를 PipeWire로 설정합니다. (pipewire 포함) |
default-mf | Windows | 기본 백엔드를 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 | 요구 사항 |
|---|---|
| macOS | Xcode 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 |
PixelBuffer | macOS 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_id | None | VideoDevice::unique_id() 값. None은 기본 장치를 의미합니다. |
width | 640 | 요청하는 너비 |
height | 480 | 요청하는 높이 |
fps | 30 | 요청하는 프레임 레이트 |
pixel_format | None | 요청하는 픽셀 포맷. None은 백엔드가 선택합니다. |
- Windows에서는
width/height/fps가 0 이하이면Error::InvalidCaptureConfig를 반환합니다. 다른 백엔드는 Rust 측에서 검증하지 않습니다. pixel_format: Some(PixelFormat::Unknown(_))는 모든 백엔드에서Error::UnsupportedPixelFormat을 반환합니다.pixel_format: Some(PixelFormat::Mjpeg)는 Linux V4L2 +mjpegfeature에서만 사용할 수 있습니다. 그 외에는Error::UnsupportedPixelFormat(PixelFormat::Mjpeg)를 반환합니다.- 요청 값은 어디까지나 희망 사항이며, 실제 값은 백엔드와 장치가 결정합니다.
PixelFormat
| 배리언트 | 내용 | to_raw() (FourCC) |
|---|---|---|
Nv12 | YUV 4:2:0 세미플레너 (Y 플레인 + UV 인터리브 플레인) | 0x3231564E (NV12) |
Yuy2 | YUV 4:2:2 팩 (Y0 U Y1 V) | 0x32595559 (YUY2) |
I420 | YUV 4:2:0 플레너 (Y / U / V 3플레인) | 0x30323449 (I420) |
Mjpeg | Motion JPEG. 디코딩은 사용자 책임 | 0x47504A4D (MJPG) |
Unknown(u32) | 알 수 없는 FourCC | 보관 중인 값 |
name()은"NV12"/"YUY2"/"I420"/"MJPEG"/"Unknown"을 반환합니다.Display는Unknown인 경우Unknown(0x????????)형식으로 값을 포함합니다.- 현재 백엔드는
Unknown을 생성하지 않습니다. 알 수 없는 포맷은formats()에서 제외되며, 해당 프레임은 콜백에 전달되지 않습니다.
프레임 메모리 레이아웃
| 포맷 | data | uv_data | stride | stride_uv |
|---|---|---|---|---|
| NV12 | Y 플레인 | Some: UV 인터리브 플레인 (ceil(height / 2) 행) | Y 1행의 바이트 수 | UV 1행의 바이트 수 |
| I420 | Y 플레인 | Some: U 플레인 바로 뒤에 V 플레인이 이어지는 연결 영역 (각 stride_uv * ceil(height / 2) 바이트) | Y 1행의 바이트 수 | U / V 각각 1행의 바이트 수 |
| YUY2 | 팩 데이터 | None | 1행의 바이트 수 | 0 |
| MJPEG | JPEG 페이로드 (길이는 data.len()) | None | 0 | 0 |
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()을 호출하세요.
스레드 안전성
| 타입 | Send | Sync | 비고 |
|---|---|---|---|
VideoDeviceList / VideoDevice | Yes | Yes | |
VideoCapture (macOS / Linux) | Yes | No | |
VideoCapture (Windows) | No | No | COM 초기화 가드를 보유하므로, 생성한 스레드에서 사용하고 해당 스레드에서 drop해야 합니다. |
VideoFrameOwned / PixelBuffer | Yes | No | Arc<VideoFrameOwned>는 스레드 간에 공유할 수 없습니다. 여러 수신 측에 배포하려면 clone하거나 Mutex로 감싸야 합니다. |
Error | Yes | Yes | Copy |
코드 예
장치 열거
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 포맷을 사용하려면
mjpegfeature가 필요합니다. 그 외의 경우 MJPEG를 요청하면Error::UnsupportedPixelFormat이 반환됩니다. mjpegfeature가 활성화되지 않은 상태에서 장치가 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 및 예제를 참조하십시오.