요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GFDL-1.1-no-invariants-or-later
file: media/v4l/capture.c
=========================
.. code-block:: c
/*
* V4L2 video capture example
*
* This program can be used and distributed without restrictions.
*
* This program is provided with the V4L2 API
* see https://linuxtv.org/docs.php for more information
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <getopt.h> /* getopt_long() */
#include <fcntl.h> /* low-level i/o */
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#define CLEAR(x) memset(&(x), 0, sizeof(x))
enum io_method {
IO_METHOD_READ,
IO_METHOD_MMAP,
IO_METHOD_USERPTR,
};
struct buffer {
void *start;
size_t length;
};
static char *dev_name;
static enum io_method io = IO_METHOD_MMAP;
static int fd = -1;
struct buffer *buffers;
static unsigned int n_buffers;
static int out_buf;
static int force_format;
static int frame_count = 70;
static void errno_exit(const char *s)
{
fprintf(stderr, "%s error %d, %s\n", s, errno, strerror(errno));
exit(EXIT_FAILURE);
}
static int xioctl(int fh, int request, void *arg)
{
int r;
do {
r = ioctl(fh, request, arg);
} while (-1 == r && EINTR == errno);
return r;
}
static void process_image(const void *p, int size)
{
if (out_buf)
fwrite(p, size, 1, stdout);
fflush(stderr);
fprintf(stderr, ".");
fflush(stdout);
}
static int read_frame(void)
{
struct v4l2_buffer buf;
unsigned int i;
switch (io) {
case IO_METHOD_READ:
if (-1 == read(fd, buffers[0].start, buffers[0].length)) {
switch (errno) {
case EAGAIN:
return 0;
case EIO:
/* Could ignore EIO, see spec. */
/* fall through */
default:
errno_exit("read");
}
}
process_image(buffers[0].start, buffers[0].length);
break;
case IO_METHOD_MMAP:
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (-1 == xioctl(fd, VIDIOC_DQBUF, &buf)) {
switch (errno) {
case EAGAIN:
return 0;
case EIO:
/* Could ignore EIO, see spec. */
/* fall through */
default:
errno_exit("VIDIOC_DQBUF");
}
}
assert(buf.index < n_buffers);
process_image(buffers[buf.index].start, buf.bytesused);
if (-1 == xioctl(fd, VIDIOC_QBUF, &buf))
errno_exit("VIDIOC_QBUF");
break;
case IO_METHOD_USERPTR:
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_USERPTR;
if (-1 == xioctl(fd, VIDIOC_DQBUF, &buf)) {
switch (errno) {
case EAGAIN:
return 0;
case EIO:
/* Could ignore EIO, see spec. */
/* fall through */
default:
errno_exit("VIDIOC_DQBUF");
}
}
for (i = 0; i < n_buffers; ++i)
if (buf.m.userptr == (unsigned long)buffers[i].start
&& buf.length == buffers[i].length)
break;
assert(i < n_buffers);
process_image((void *)buf.m.userptr, buf.bytesused);
if (-1 == xioctl(fd, VIDIOC_QBUF, &buf))
errno_exit("VIDIOC_QBUF");
break;
}
return 1;
}
static void mainloop(void)
{
unsigned int count;
count = frame_count;
while (count-- > 0) {
for (;;) {
fd_set fds;
struct timeval tv;
int r;
FD_ZERO(&fds);
FD_SET(fd, &fds);
/* Timeout. */
tv.tv_sec = 2;
tv.tv_usec = 0;
r = select(fd + 1, &fds, NULL, NULL, &tv);
if (-1 == r) {
if (EINTR == errno)
continue;
errno_exit("select");
}
if (0 == r) {
fprintf(stderr, "select timeout\n");
exit(EXIT_FAILURE);
}
if (read_frame())
break;
/* EAGAIN - continue select loop. */
}
}
}
static void stop_capturing(void)
{
enum v4l2_buf_type type;
switch (io) {
case IO_METHOD_READ:
/* Nothing to do. */
break;
case IO_METHOD_MMAP:
case IO_METHOD_USERPTR:
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == xioctl(fd, VIDIOC_STREAMOFF, &type))
errno_exit("VIDIOC_STREAMOFF");
break;
}
}
static void start_capturing(void)
{
unsigned int i;
enum v4l2_buf_type type;
switch (io) {
case IO_METHOD_READ:
/* Nothing to do. */
break;
case IO_METHOD_MMAP:
for (i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf;
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (-1 == xioctl(fd, VIDIOC_QBUF, &buf))
errno_exit("VIDIOC_QBUF");
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == xioctl(fd, VIDIOC_STREAMON, &type))
errno_exit("VIDIOC_STREAMON");
break;
case IO_METHOD_USERPTR:
for (i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf;
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_USERPTR;
buf.index = i;
buf.m.userptr = (unsigned long)buffers[i].start;
buf.length = buffers[i].length;
if (-1 == xioctl(fd, VIDIOC_QBUF, &buf))
errno_exit("VIDIOC_QBUF");
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == xioctl(fd, VIDIOC_STREAMON, &type))
errno_exit("VIDIOC_STREAMON");
break;
}
}
static void uninit_device(void)
{
unsigned int i;
switch (io) {
case IO_METHOD_READ:
free(buffers[0].start);
break;
case IO_METHOD_MMAP:
for (i = 0; i < n_buffers; ++i)
if (-1 == munmap(buffers[i].start, buffers[i].length))
errno_exit("munmap");
break;
case IO_METHOD_USERPTR:
for (i = 0; i < n_buffers; ++i)
free(buffers[i].start);
break;
}
free(buffers);
}
static void init_read(unsigned int buffer_size)
{
buffers = calloc(1, sizeof(*buffers));
if (!buffers) {
fprintf(stderr, "Out of memory\n");
exit(EXIT_FAILURE);
}
buffers[0].length = buffer_size;
buffers[0].start = malloc(buffer_size);
if (!buffers[0].start) {
fprintf(stderr, "Out of memory\n");
exit(EXIT_FAILURE);
}
}
static void init_mmap(void)
{
struct v4l2_requestbuffers req;
CLEAR(req);
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (-1 == xioctl(fd, VIDIOC_REQBUFS, &req)) {
if (EINVAL == errno) {
fprintf(stderr, "%s does not support "
"memory mapping\n", dev_name);
exit(EXIT_FAILURE);
} else {
errno_exit("VIDIOC_REQBUFS");
}
}
if (req.count < 2) {
fprintf(stderr, "Insufficient buffer memory on %s\n",
dev_name);
exit(EXIT_FAILURE);
}
buffers = calloc(req.count, sizeof(*buffers));
if (!buffers) {
fprintf(stderr, "Out of memory\n");
exit(EXIT_FAILURE);
}
for (n_buffers = 0; n_buffers < req.count; ++n_buffers) {
struct v4l2_buffer buf;
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = n_buffers;
if (-1 == xioctl(fd, VIDIOC_QUERYBUF, &buf))
errno_exit("VIDIOC_QUERYBUF");
buffers[n_buffers].length = buf.length;
buffers[n_buffers].start =
mmap(NULL /* start anywhere */,
buf.length,
PROT_READ | PROT_WRITE /* required */,
MAP_SHARED /* recommended */,
fd, buf.m.offset);
if (MAP_FAILED == buffers[n_buffers].start)
errno_exit("mmap");
}
}
static void init_userp(unsigned int buffer_size)
{
struct v4l2_requestbuffers req;
CLEAR(req);
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_USERPTR;
if (-1 == xioctl(fd, VIDIOC_REQBUFS, &req)) {
if (EINVAL == errno) {
fprintf(stderr, "%s does not support "
"user pointer i/o\n", dev_name);
exit(EXIT_FAILURE);
} else {
errno_exit("VIDIOC_REQBUFS");
}
}
buffers = calloc(4, sizeof(*buffers));
if (!buffers) {
fprintf(stderr, "Out of memory\n");
exit(EXIT_FAILURE);
}
for (n_buffers = 0; n_buffers < 4; ++n_buffers) {
buffers[n_buffers].length = buffer_size;
buffers[n_buffers].start = malloc(buffer_size);
if (!buffers[n_buffers].start) {
fprintf(stderr, "Out of memory\n");
exit(EXIT_FAILURE);
}
}
}
static void init_device(void)
{
struct v4l2_capability cap;
struct v4l2_cropcap cropcap;
struct v4l2_crop crop;
struct v4l2_format fmt;
unsigned int min;
if (-1 == xioctl(fd, VIDIOC_QUERYCAP, &cap)) {
if (EINVAL == errno) {
fprintf(stderr, "%s is no V4L2 device\n",
dev_name);
exit(EXIT_FAILURE);
} else {
errno_exit("VIDIOC_QUERYCAP");
}
}
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
fprintf(stderr, "%s is no video capture device\n",
dev_name);
exit(EXIT_FAILURE);
}
switch (io) {
case IO_METHOD_READ:
if (!(cap.capabilities & V4L2_CAP_READWRITE)) {
fprintf(stderr, "%s does not support read i/o\n",
dev_name);
exit(EXIT_FAILURE);
}
break;
case IO_METHOD_MMAP:
case IO_METHOD_USERPTR:
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
fprintf(stderr, "%s does not support streaming i/o\n",
dev_name);
exit(EXIT_FAILURE);
}
break;
}
/* Select video input, video standard and tune here. */
CLEAR(cropcap);
cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 == xioctl(fd, VIDIOC_CROPCAP, &cropcap)) {
crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
crop.c = cropcap.defrect; /* reset to default */
if (-1 == xioctl(fd, VIDIOC_S_CROP, &crop)) {
switch (errno) {
case EINVAL:
/* Cropping not supported. */
break;
default:
/* Errors ignored. */
break;
}
}
} else {
/* Errors ignored. */
}
CLEAR(fmt);
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (force_format) {
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (-1 == xioctl(fd, VIDIOC_S_FMT, &fmt))
errno_exit("VIDIOC_S_FMT");
/* Note VIDIOC_S_FMT may change width and height. */
} else {
/* Preserve original settings as set by v4l2-ctl for example */
if (-1 == xioctl(fd, VIDIOC_G_FMT, &fmt))
errno_exit("VIDIOC_G_FMT");
}
/* Buggy driver paranoia. */
min = fmt.fmt.pix.width * 2;
if (fmt.fmt.pix.bytesperline < min)
fmt.fmt.pix.bytesperline = min;
min = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height;
if (fmt.fmt.pix.sizeimage < min)
fmt.fmt.pix.sizeimage = min;
switch (io) {
case IO_METHOD_READ:
init_read(fmt.fmt.pix.sizeimage);
break;
case IO_METHOD_MMAP:
init_mmap();
break;
case IO_METHOD_USERPTR:
init_userp(fmt.fmt.pix.sizeimage);
break;
}
}
static void close_device(void)
{
if (-1 == close(fd))
errno_exit("close");
fd = -1;
}
static void open_device(void)
{
struct stat st;
if (-1 == stat(dev_name, &st)) {
fprintf(stderr, "Cannot identify '%s': %d, %s\n",
dev_name, errno, strerror(errno));
exit(EXIT_FAILURE);
}
if (!S_ISCHR(st.st_mode)) {
fprintf(stderr, "%s is no device\n", dev_name);
exit(EXIT_FAILURE);
}
fd = open(dev_name, O_RDWR /* required */ | O_NONBLOCK, 0);
if (-1 == fd) {
fprintf(stderr, "Cannot open '%s': %d, %s\n",
dev_name, errno, strerror(errno));
exit(EXIT_FAILURE);
}
}
static void usage(FILE *fp, int argc, char **argv)
{
fprintf(fp,
"Usage: %s [options]\n\n"
"Version 1.3\n"
"Options:\n"
"-d | --device name Video device name [%s]\n"
"-h | --help Print this message\n"
"-m | --mmap Use memory mapped buffers [default]\n"
"-r | --read Use read() calls\n"
"-u | --userp Use application allocated buffers\n"
"-o | --output Outputs stream to stdout\n"
"-f | --format Force format to 640x480 YUYV\n"
"-c | --count Number of frames to grab [%i]\n"
"",
argv[0], dev_name, frame_count);
}
static const char short_options[] = "d:hmruofc:";
static const struct option
long_options[] = {
{ "device", required_argument, NULL, 'd' },
{ "help", no_argument, NULL, 'h' },
{ "mmap", no_argument, NULL, 'm' },
{ "read", no_argument, NULL, 'r' },
{ "userp", no_argument, NULL, 'u' },
{ "output", no_argument, NULL, 'o' },
{ "format", no_argument, NULL, 'f' },
{ "count", required_argument, NULL, 'c' },
{ 0, 0, 0, 0 }
};
int main(int argc, char **argv)
{
dev_name = "/dev/video0";
for (;;) {
int idx;
int c;
c = getopt_long(argc, argv,
short_options, long_options, &idx);
if (-1 == c)
break;
switch (c) {
case 0: /* getopt_long() flag */
break;
case 'd':
dev_name = optarg;
break;
case 'h':
usage(stdout, argc, argv);
exit(EXIT_SUCCESS);
case 'm':
io = IO_METHOD_MMAP;
break;
case 'r':
io = IO_METHOD_READ;
break;
case 'u':
io = IO_METHOD_USERPTR;
break;
case 'o':
out_buf++;
break;
case 'f':
force_format++;
break;
case 'c':
errno = 0;
frame_count = strtol(optarg, NULL, 0);
if (errno)
errno_exit(optarg);
break;
default:
usage(stderr, argc, argv);
exit(EXIT_FAILURE);
}
}
open_device();
init_device();
start_capturing();
mainloop();
stop_capturing();
uninit_device();
close_device();
fprintf(stderr, "\n");
return 0;
}
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
라이선스, 헤더와 기본 선언
1-35이 문서는 `media/v4l/capture.c` 파일 전체를 C 코드 블록으로 제공합니다. 머리말은 이 프로그램을 제한 없이 사용하고 배포할 수 있으며 V4L2 API와 함께 제공된 예제임을 밝히고, 추가 정보는 LinuxTV 문서를 참조하도록 안내합니다.
표준 입출력, 메모리, 문자열과 단언 기능에 더해 명령행 옵션, 파일 제어, 오류 번호, 파일 상태, 시간, 메모리 매핑과 ioctl 인터페이스를 포함합니다. V4L2 ABI 정의는 `<linux/videodev2.h>`에서 가져옵니다.
`CLEAR(x)`는 `memset()`으로 구조체 전체를 0으로 초기화합니다. V4L2 ioctl에 전달하는 구조체의 예약 필드를 0으로 유지하기 위한 핵심 준비 단계입니다.
예제가 사용하는 사용자 공간 인터페이스를 기능별로 묶었습니다.
.. SPDX-License-Identifier: GFDL-1.1-no-invariants-or-later
file: media/v4l/capture.c
=========================
.. code-block:: c
/*
* V4L2 video capture example
*
* This program can be used and distributed without restrictions.
*
* This program is provided with the V4L2 API
* see https://linuxtv.org/docs.php for more information
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <getopt.h> /* getopt_long() */
#include <fcntl.h> /* low-level i/o */
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#define CLEAR(x) memset(&(x), 0, sizeof(x))
I/O 방식, 전역 상태와 공통 함수
36-83`enum io_method`는 세 캡처 방식을 구분합니다. `IO_METHOD_READ`는 `read()`로 복사하고, `IO_METHOD_MMAP`은 드라이버가 할당한 버퍼를 매핑하며, `IO_METHOD_USERPTR`은 애플리케이션이 할당한 버퍼를 드라이버에 제공합니다.
`struct buffer`는 각 버퍼의 시작 주소와 길이를 저장합니다. 기본 장치는 뒤에서 `/dev/video0`으로 정하고, 기본 I/O 방식은 `MMAP`, 기본 프레임 수는 70입니다. `out_buf`와 `force_format`은 각각 표준 출력 전송과 강제 형식 설정의 활성화 횟수를 담습니다.
`errno_exit()`는 실패한 작업 이름, `errno` 값과 설명을 표준 오류에 출력한 뒤 프로그램을 종료합니다. `xioctl()`은 시그널로 중단된 `ioctl()`이 `EINTR`을 반환하면 같은 요청을 다시 시도합니다.
`process_image()`는 `--output`이 지정됐을 때 프레임 payload를 `stdout`에 그대로 쓰고, 처리한 프레임마다 진행 표시인 마침표를 `stderr`에 출력합니다. 두 스트림을 명시적으로 flush하여 출력 지연을 줄입니다.
동일한 프레임 처리 루틴 앞뒤에서 버퍼 소유권 전달 방식만 달라집니다.
enum io_method {
IO_METHOD_READ,
IO_METHOD_MMAP,
IO_METHOD_USERPTR,
};
struct buffer {
void *start;
size_t length;
};
static char *dev_name;
static enum io_method io = IO_METHOD_MMAP;
static int fd = -1;
struct buffer *buffers;
static unsigned int n_buffers;
static int out_buf;
static int force_format;
static int frame_count = 70;
static void errno_exit(const char *s)
{
fprintf(stderr, "%s error %d, %s\n", s, errno, strerror(errno));
exit(EXIT_FAILURE);
}
static int xioctl(int fh, int request, void *arg)
{
int r;
do {
r = ioctl(fh, request, arg);
} while (-1 == r && EINTR == errno);
return r;
}
static void process_image(const void *p, int size)
{
if (out_buf)
fwrite(p, size, 1, stdout);
fflush(stderr);
fprintf(stderr, ".");
fflush(stdout);
}
프레임 한 개 읽기
84-174`read_frame()`은 선택한 I/O 방식에 따라 프레임 하나를 얻습니다. 성공하면 1, 비차단 호출에서 아직 데이터가 없는 `EAGAIN`이면 0을 반환하여 상위 루프가 다시 기다리게 합니다.
READ 방식은 첫 번째 사용자 버퍼 전체 길이만큼 `read()`하고 그 메모리를 처리합니다. 코드 주석은 `EIO`를 무시할 수도 있다고 설명하지만, 이 예제 구현은 `fall through`하여 다른 치명적 오류와 같이 `errno_exit()`로 종료합니다.
MMAP 방식은 `struct v4l2_buffer`를 초기화하고 `V4L2_BUF_TYPE_VIDEO_CAPTURE`와 `V4L2_MEMORY_MMAP`을 지정합니다. `VIDIOC_DQBUF`로 완료된 버퍼를 꺼낸 뒤 인덱스 범위를 단언하고, 실제 payload 길이인 `buf.bytesused`만 처리한 다음 `VIDIOC_QBUF`로 같은 버퍼를 재등록합니다.
USERPTR 방식도 `VIDIOC_DQBUF`를 사용하지만 메모리 형식은 `V4L2_MEMORY_USERPTR`입니다. 반환된 `buf.m.userptr`와 `buf.length`가 애플리케이션의 어느 버퍼와 일치하는지 찾고, 일치 항목이 있음을 단언한 후 payload를 처리하고 재등록합니다.
각 방식의 취득 호출, 데이터 주소와 반환 절차입니다.
MMAP과 USERPTR이 공유하는 dequeue/queue 순환입니다.
static int read_frame(void)
{
struct v4l2_buffer buf;
unsigned int i;
switch (io) {
case IO_METHOD_READ:
if (-1 == read(fd, buffers[0].start, buffers[0].length)) {
switch (errno) {
case EAGAIN:
return 0;
case EIO:
/* Could ignore EIO, see spec. */
/* fall through */
default:
errno_exit("read");
}
}
process_image(buffers[0].start, buffers[0].length);
break;
case IO_METHOD_MMAP:
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (-1 == xioctl(fd, VIDIOC_DQBUF, &buf)) {
switch (errno) {
case EAGAIN:
return 0;
case EIO:
/* Could ignore EIO, see spec. */
/* fall through */
default:
errno_exit("VIDIOC_DQBUF");
}
}
assert(buf.index < n_buffers);
process_image(buffers[buf.index].start, buf.bytesused);
if (-1 == xioctl(fd, VIDIOC_QBUF, &buf))
errno_exit("VIDIOC_QBUF");
break;
case IO_METHOD_USERPTR:
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_USERPTR;
if (-1 == xioctl(fd, VIDIOC_DQBUF, &buf)) {
switch (errno) {
case EAGAIN:
return 0;
case EIO:
/* Could ignore EIO, see spec. */
/* fall through */
default:
errno_exit("VIDIOC_DQBUF");
}
}
for (i = 0; i < n_buffers; ++i)
if (buf.m.userptr == (unsigned long)buffers[i].start
&& buf.length == buffers[i].length)
break;
assert(i < n_buffers);
process_image((void *)buf.m.userptr, buf.bytesused);
if (-1 == xioctl(fd, VIDIOC_QBUF, &buf))
errno_exit("VIDIOC_QBUF");
break;
}
return 1;
}
select 기반 캡처 루프
175-213`mainloop()`는 `frame_count`만큼 프레임을 처리합니다. 각 프레임마다 장치 파일 디스크립터를 읽기 집합에 넣고 `select()`로 준비 상태를 기다립니다.
대기 제한은 2초입니다. `select()`가 `EINTR`로 중단되면 다시 기다리고, 그 밖의 오류는 종료합니다. 2초 안에 준비되지 않으면 `select timeout`을 출력하고 실패로 끝냅니다.
장치가 준비되어도 비차단 `read()`나 `VIDIOC_DQBUF`가 `EAGAIN`을 반환할 수 있으므로 `read_frame()`이 0이면 내부 select 루프를 반복합니다. 프레임을 실제로 처리해 1을 반환할 때만 다음 프레임 횟수로 넘어갑니다.
비차단 장치를 안정적으로 소비하는 제어 흐름입니다.
static void mainloop(void)
{
unsigned int count;
count = frame_count;
while (count-- > 0) {
for (;;) {
fd_set fds;
struct timeval tv;
int r;
FD_ZERO(&fds);
FD_SET(fd, &fds);
/* Timeout. */
tv.tv_sec = 2;
tv.tv_usec = 0;
r = select(fd + 1, &fds, NULL, NULL, &tv);
if (-1 == r) {
if (EINTR == errno)
continue;
errno_exit("select");
}
if (0 == r) {
fprintf(stderr, "select timeout\n");
exit(EXIT_FAILURE);
}
if (read_frame())
break;
/* EAGAIN - continue select loop. */
}
}
}
스트리밍 시작과 정지
214-279`stop_capturing()`에서 READ 방식은 별도 스트림 상태가 없으므로 수행할 작업이 없습니다. MMAP과 USERPTR 방식은 `V4L2_BUF_TYPE_VIDEO_CAPTURE`에 대해 `VIDIOC_STREAMOFF`를 호출합니다.
`start_capturing()`도 READ 방식에는 작업이 없습니다. MMAP은 각 인덱스의 버퍼에 type, memory와 index를 채워 `VIDIOC_QBUF`로 모두 등록한 뒤 `VIDIOC_STREAMON`을 호출합니다.
USERPTR은 각 버퍼의 `m.userptr`와 `length`까지 구조체에 넣어 등록합니다. 모든 버퍼가 드라이버 큐에 들어간 다음에만 `VIDIOC_STREAMON`으로 캡처를 시작합니다.
버퍼 기반 I/O에서 큐가 먼저 준비되어야 합니다.
static void stop_capturing(void)
{
enum v4l2_buf_type type;
switch (io) {
case IO_METHOD_READ:
/* Nothing to do. */
break;
case IO_METHOD_MMAP:
case IO_METHOD_USERPTR:
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == xioctl(fd, VIDIOC_STREAMOFF, &type))
errno_exit("VIDIOC_STREAMOFF");
break;
}
}
static void start_capturing(void)
{
unsigned int i;
enum v4l2_buf_type type;
switch (io) {
case IO_METHOD_READ:
/* Nothing to do. */
break;
case IO_METHOD_MMAP:
for (i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf;
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (-1 == xioctl(fd, VIDIOC_QBUF, &buf))
errno_exit("VIDIOC_QBUF");
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == xioctl(fd, VIDIOC_STREAMON, &type))
errno_exit("VIDIOC_STREAMON");
break;
case IO_METHOD_USERPTR:
for (i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf;
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_USERPTR;
buf.index = i;
buf.m.userptr = (unsigned long)buffers[i].start;
buf.length = buffers[i].length;
if (-1 == xioctl(fd, VIDIOC_QBUF, &buf))
errno_exit("VIDIOC_QBUF");
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == xioctl(fd, VIDIOC_STREAMON, &type))
errno_exit("VIDIOC_STREAMON");
break;
}
}
자원 해제와 READ 버퍼 초기화
280-321`uninit_device()`는 I/O 방식에 맞게 메모리를 정리합니다. READ는 단일 힙 버퍼를 `free()`하고, MMAP은 모든 매핑을 `munmap()`하며, USERPTR은 애플리케이션이 할당한 각 버퍼를 `free()`합니다. 마지막에는 버퍼 설명 배열 자체도 해제합니다.
`init_read()`는 설명 구조체 하나를 `calloc()`하고 `sizeimage`에 해당하는 데이터 버퍼를 `malloc()`합니다. 두 할당 중 하나라도 실패하면 `Out of memory`를 알리고 즉시 종료합니다.
할당 방식과 대칭을 이루는 정리 호출입니다.
static void uninit_device(void)
{
unsigned int i;
switch (io) {
case IO_METHOD_READ:
free(buffers[0].start);
break;
case IO_METHOD_MMAP:
for (i = 0; i < n_buffers; ++i)
if (-1 == munmap(buffers[i].start, buffers[i].length))
errno_exit("munmap");
break;
case IO_METHOD_USERPTR:
for (i = 0; i < n_buffers; ++i)
free(buffers[i].start);
break;
}
free(buffers);
}
static void init_read(unsigned int buffer_size)
{
buffers = calloc(1, sizeof(*buffers));
if (!buffers) {
fprintf(stderr, "Out of memory\n");
exit(EXIT_FAILURE);
}
buffers[0].length = buffer_size;
buffers[0].start = malloc(buffer_size);
if (!buffers[0].start) {
fprintf(stderr, "Out of memory\n");
exit(EXIT_FAILURE);
}
}
MMAP 버퍼 초기화
322-379`init_mmap()`은 `struct v4l2_requestbuffers`를 0으로 초기화한 뒤 네 개의 VIDEO_CAPTURE/MMAP 버퍼를 `VIDIOC_REQBUFS`로 요청합니다. 드라이버가 이 방식을 지원하지 않아 `EINVAL`을 반환하면 메모리 매핑 미지원 진단을 출력합니다.
드라이버가 두 개 미만의 버퍼만 제공하면 연속 캡처에 부족하다고 판단해 종료합니다. 실제로 제공된 `req.count`만큼 설명 배열을 할당합니다.
각 인덱스에 `VIDIOC_QUERYBUF`를 호출하여 길이와 장치 메모리 오프셋을 얻고, 해당 구간을 `PROT_READ | PROT_WRITE`, `MAP_SHARED`로 매핑합니다. 원문의 주석대로 시작 주소는 `NULL`로 두어 임의 위치를 허용하고, 읽기·쓰기 권한은 필수이며 공유 매핑은 권장됩니다.
V4L2 드라이버 버퍼를 사용자 공간에 연결하는 설정입니다.
static void init_mmap(void)
{
struct v4l2_requestbuffers req;
CLEAR(req);
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (-1 == xioctl(fd, VIDIOC_REQBUFS, &req)) {
if (EINVAL == errno) {
fprintf(stderr, "%s does not support "
"memory mapping\n", dev_name);
exit(EXIT_FAILURE);
} else {
errno_exit("VIDIOC_REQBUFS");
}
}
if (req.count < 2) {
fprintf(stderr, "Insufficient buffer memory on %s\n",
dev_name);
exit(EXIT_FAILURE);
}
buffers = calloc(req.count, sizeof(*buffers));
if (!buffers) {
fprintf(stderr, "Out of memory\n");
exit(EXIT_FAILURE);
}
for (n_buffers = 0; n_buffers < req.count; ++n_buffers) {
struct v4l2_buffer buf;
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = n_buffers;
if (-1 == xioctl(fd, VIDIOC_QUERYBUF, &buf))
errno_exit("VIDIOC_QUERYBUF");
buffers[n_buffers].length = buf.length;
buffers[n_buffers].start =
mmap(NULL /* start anywhere */,
buf.length,
PROT_READ | PROT_WRITE /* required */,
MAP_SHARED /* recommended */,
fd, buf.m.offset);
if (MAP_FAILED == buffers[n_buffers].start)
errno_exit("mmap");
}
}
USERPTR 버퍼 초기화
380-417`init_userp()`은 네 개의 VIDEO_CAPTURE/USERPTR 버퍼 사용 의사를 `VIDIOC_REQBUFS`로 드라이버에 알립니다. `EINVAL`이면 사용자 포인터 I/O를 지원하지 않는 장치로 처리합니다.
데이터 메모리는 드라이버가 아니라 애플리케이션이 확보합니다. 버퍼 설명 배열 네 개를 만든 뒤 각 항목에 `buffer_size`를 기록하고 같은 크기의 메모리를 `malloc()`합니다.
이 예제는 반환된 `req.count` 대신 고정된 네 개를 할당합니다. 이후 `start_capturing()`이 각 주소와 길이를 `v4l2_buffer.m.userptr`와 `length`에 넣어 드라이버 큐로 전달합니다.
static void init_userp(unsigned int buffer_size)
{
struct v4l2_requestbuffers req;
CLEAR(req);
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_USERPTR;
if (-1 == xioctl(fd, VIDIOC_REQBUFS, &req)) {
if (EINVAL == errno) {
fprintf(stderr, "%s does not support "
"user pointer i/o\n", dev_name);
exit(EXIT_FAILURE);
} else {
errno_exit("VIDIOC_REQBUFS");
}
}
buffers = calloc(4, sizeof(*buffers));
if (!buffers) {
fprintf(stderr, "Out of memory\n");
exit(EXIT_FAILURE);
}
for (n_buffers = 0; n_buffers < 4; ++n_buffers) {
buffers[n_buffers].length = buffer_size;
buffers[n_buffers].start = malloc(buffer_size);
if (!buffers[n_buffers].start) {
fprintf(stderr, "Out of memory\n");
exit(EXIT_FAILURE);
}
}
}
장치 기능과 기본 크롭 설정
418-486`init_device()`는 먼저 `VIDIOC_QUERYCAP`으로 장치 기능을 조회합니다. ioctl 자체가 `EINVAL`이면 해당 파일은 V4L2 장치가 아니며, `V4L2_CAP_VIDEO_CAPTURE`가 없으면 비디오 캡처 장치가 아닙니다.
READ 방식에는 `V4L2_CAP_READWRITE`가 필요하고, MMAP과 USERPTR에는 `V4L2_CAP_STREAMING`이 필요합니다. 선택한 I/O 방식과 장치 기능이 맞지 않으면 명시적인 진단과 함께 종료합니다.
비디오 입력, 비디오 표준과 튜너 선택은 주석으로 확장 지점만 남겨 둡니다. 이어 `VIDIOC_CROPCAP`으로 기본 크롭 사각형을 얻고 `VIDIOC_S_CROP`으로 적용을 시도합니다.
크롭은 선택 기능이므로 지원하지 않는 `EINVAL`과 기타 설정 오류를 모두 무시합니다. `crop.c = cropcap.defrect`의 주석은 크롭 영역을 장치 기본값으로 되돌린다는 뜻입니다.
QUERYCAP 결과에서 확인하는 capability 비트입니다.
static void init_device(void)
{
struct v4l2_capability cap;
struct v4l2_cropcap cropcap;
struct v4l2_crop crop;
struct v4l2_format fmt;
unsigned int min;
if (-1 == xioctl(fd, VIDIOC_QUERYCAP, &cap)) {
if (EINVAL == errno) {
fprintf(stderr, "%s is no V4L2 device\n",
dev_name);
exit(EXIT_FAILURE);
} else {
errno_exit("VIDIOC_QUERYCAP");
}
}
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
fprintf(stderr, "%s is no video capture device\n",
dev_name);
exit(EXIT_FAILURE);
}
switch (io) {
case IO_METHOD_READ:
if (!(cap.capabilities & V4L2_CAP_READWRITE)) {
fprintf(stderr, "%s does not support read i/o\n",
dev_name);
exit(EXIT_FAILURE);
}
break;
case IO_METHOD_MMAP:
case IO_METHOD_USERPTR:
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
fprintf(stderr, "%s does not support streaming i/o\n",
dev_name);
exit(EXIT_FAILURE);
}
break;
}
/* Select video input, video standard and tune here. */
CLEAR(cropcap);
cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 == xioctl(fd, VIDIOC_CROPCAP, &cropcap)) {
crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
crop.c = cropcap.defrect; /* reset to default */
if (-1 == xioctl(fd, VIDIOC_S_CROP, &crop)) {
switch (errno) {
case EINVAL:
/* Cropping not supported. */
break;
default:
/* Errors ignored. */
break;
}
}
} else {
/* Errors ignored. */
}
픽셀 형식과 버퍼 크기 결정
487-529형식 구조체의 type은 `V4L2_BUF_TYPE_VIDEO_CAPTURE`입니다. `--format`이 지정되면 640x480, `V4L2_PIX_FMT_YUYV`, `V4L2_FIELD_INTERLACED`를 요청하고 `VIDIOC_S_FMT`으로 적용합니다.
드라이버는 `VIDIOC_S_FMT` 과정에서 폭과 높이를 조정할 수 있으므로 반환된 구조체를 최종값으로 사용해야 합니다. 강제 형식이 아니면 `v4l2-ctl` 등으로 미리 설정된 값을 보존하기 위해 `VIDIOC_G_FMT`만 호출합니다.
결함이 있는 드라이버에 대비해 `bytesperline`이 폭의 두 배보다 작지 않도록 하고, `sizeimage`가 `bytesperline * height`보다 작지 않도록 보정합니다. YUYV가 픽셀당 2바이트라는 전제에 맞춘 방어 코드입니다.
최종 `sizeimage`를 기준으로 READ는 `init_read()`, MMAP은 `init_mmap()`, USERPTR은 `init_userp()`를 호출하여 선택한 메모리 모델의 버퍼를 준비합니다.
최종 형식이 결정된 뒤 메모리를 준비합니다.
CLEAR(fmt);
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (force_format) {
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (-1 == xioctl(fd, VIDIOC_S_FMT, &fmt))
errno_exit("VIDIOC_S_FMT");
/* Note VIDIOC_S_FMT may change width and height. */
} else {
/* Preserve original settings as set by v4l2-ctl for example */
if (-1 == xioctl(fd, VIDIOC_G_FMT, &fmt))
errno_exit("VIDIOC_G_FMT");
}
/* Buggy driver paranoia. */
min = fmt.fmt.pix.width * 2;
if (fmt.fmt.pix.bytesperline < min)
fmt.fmt.pix.bytesperline = min;
min = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height;
if (fmt.fmt.pix.sizeimage < min)
fmt.fmt.pix.sizeimage = min;
switch (io) {
case IO_METHOD_READ:
init_read(fmt.fmt.pix.sizeimage);
break;
case IO_METHOD_MMAP:
init_mmap();
break;
case IO_METHOD_USERPTR:
init_userp(fmt.fmt.pix.sizeimage);
break;
}
}
장치 파일 열기와 닫기
530-561`open_device()`는 먼저 `stat()`으로 지정 경로를 확인합니다. 식별할 수 없거나 문자 장치가 아니면 실패하며, 실제 V4L2 기능 확인은 이후 `VIDIOC_QUERYCAP`이 담당합니다.
장치는 필수인 `O_RDWR`와 비차단 동작을 위한 `O_NONBLOCK`으로 엽니다. 비차단 플래그 덕분에 아직 프레임이 없을 때 `read()`와 `VIDIOC_DQBUF`가 `EAGAIN`을 반환하고, 상위 select 루프가 다시 대기할 수 있습니다.
`close_device()`는 파일 디스크립터를 닫고 전역 `fd`를 -1로 되돌립니다. 닫기 실패도 다른 시스템 호출과 동일하게 치명적 오류로 처리합니다.
예제가 사용자에게 출력하는 주요 영어 메시지의 의미입니다.
static void close_device(void)
{
if (-1 == close(fd))
errno_exit("close");
fd = -1;
}
static void open_device(void)
{
struct stat st;
if (-1 == stat(dev_name, &st)) {
fprintf(stderr, "Cannot identify '%s': %d, %s\n",
dev_name, errno, strerror(errno));
exit(EXIT_FAILURE);
}
if (!S_ISCHR(st.st_mode)) {
fprintf(stderr, "%s is no device\n", dev_name);
exit(EXIT_FAILURE);
}
fd = open(dev_name, O_RDWR /* required */ | O_NONBLOCK, 0);
if (-1 == fd) {
fprintf(stderr, "Cannot open '%s': %d, %s\n",
dev_name, errno, strerror(errno));
exit(EXIT_FAILURE);
}
}
사용법과 명령행 옵션
562-595`usage()`는 프로그램 버전 1.3과 옵션 도움말을 출력합니다. 기본 장치 이름과 기본 프레임 수는 현재 전역 변수 값을 문자열에 삽입합니다.
짧은 옵션 문자열 `d:hmruofc:`에서 콜론은 `-d`와 `-c`가 인자를 요구한다는 뜻입니다. `long_options`는 각 긴 옵션을 같은 단일 문자 분기로 연결합니다.
도움말 문구를 한국어로 옮긴 표입니다.
static void usage(FILE *fp, int argc, char **argv)
{
fprintf(fp,
"Usage: %s [options]\n\n"
"Version 1.3\n"
"Options:\n"
"-d | --device name Video device name [%s]\n"
"-h | --help Print this message\n"
"-m | --mmap Use memory mapped buffers [default]\n"
"-r | --read Use read() calls\n"
"-u | --userp Use application allocated buffers\n"
"-o | --output Outputs stream to stdout\n"
"-f | --format Force format to 640x480 YUYV\n"
"-c | --count Number of frames to grab [%i]\n"
"",
argv[0], dev_name, frame_count);
}
static const char short_options[] = "d:hmruofc:";
static const struct option
long_options[] = {
{ "device", required_argument, NULL, 'd' },
{ "help", no_argument, NULL, 'h' },
{ "mmap", no_argument, NULL, 'm' },
{ "read", no_argument, NULL, 'r' },
{ "userp", no_argument, NULL, 'u' },
{ "output", no_argument, NULL, 'o' },
{ "format", no_argument, NULL, 'f' },
{ "count", required_argument, NULL, 'c' },
{ 0, 0, 0, 0 }
};
옵션 처리와 프로그램 수명 주기
596-664`main()`은 기본 장치를 `/dev/video0`으로 지정하고 `getopt_long()`으로 옵션을 반복 해석합니다. `-m`, `-r`, `-u`는 마지막에 지정된 I/O 방식을 선택하고, `-o`와 `-f`는 해당 기능을 활성화합니다.
`-c`의 인자는 `strtol()`로 변환합니다. 호출 전에 `errno`를 0으로 만들고 변환 후 오류가 설정되면 해당 인자 문자열을 작업 이름으로 하여 종료합니다. 인식하지 못한 옵션은 사용법을 표준 오류에 출력합니다.
옵션 처리가 끝나면 장치를 열고 초기화하며, 캡처를 시작해 지정한 프레임 수만큼 루프를 수행합니다. 그 뒤 스트리밍 정지, 버퍼 해제, 장치 닫기를 역순으로 실행하고 표준 오류에 줄바꿈을 출력한 뒤 성공으로 반환합니다.
`case 0`의 주석은 `getopt_long()`의 flag 기반 옵션을 위한 분기라는 뜻입니다. 이 예제의 모든 실제 옵션은 문자 값을 반환하므로 해당 분기에서는 추가 작업을 하지 않습니다.
main()이 호출하는 준비, 실행과 정리 순서입니다.
int main(int argc, char **argv)
{
dev_name = "/dev/video0";
for (;;) {
int idx;
int c;
c = getopt_long(argc, argv,
short_options, long_options, &idx);
if (-1 == c)
break;
switch (c) {
case 0: /* getopt_long() flag */
break;
case 'd':
dev_name = optarg;
break;
case 'h':
usage(stdout, argc, argv);
exit(EXIT_SUCCESS);
case 'm':
io = IO_METHOD_MMAP;
break;
case 'r':
io = IO_METHOD_READ;
break;
case 'u':
io = IO_METHOD_USERPTR;
break;
case 'o':
out_buf++;
break;
case 'f':
force_format++;
break;
case 'c':
errno = 0;
frame_count = strtol(optarg, NULL, 0);
if (errno)
errno_exit(optarg);
break;
default:
usage(stderr, argc, argv);
exit(EXIT_FAILURE);
}
}
open_device();
init_device();
start_capturing();
mainloop();
stop_capturing();
uninit_device();
close_device();
fprintf(stderr, "\n");
return 0;
}
요약·해설
capture.c.rst:1-664V4L2 캡처 애플리케이션의 기준 구현입니다. 장치 capability 검사, 형식 협상, 세 버퍼 모델의 할당과 큐잉, 비차단 select 루프, 오류 처리와 대칭적인 자원 해제를 한 파일에서 연결해 보여 줍니다.