Hướng dẫn sử dụng camera laptop với V4L2 trên Ubuntu

Để sử dụng camera laptop trên Ubuntu thông qua V4L2, cần thực hiện các bước cấu hình và lập trình cơ bản. Bài viết này trình bày chi tiết quy trình từ thiết lập môi trường ảo hóa đến thu thập dữ liệu video.

1. Thiết lập máy ảo (nếu sử dụng)

Nếu bạn đang chạy Ubuntu trên máy ảo (ví dụ VirtualBox), cần bổ sung USB controller phiên bản 3.1 và thêm thiết bị camera vào danh sách USB được chia sẻ.

2. Kiểm tra camera

Sau khi cài đặt, mở terminal và chạy lệnh cheese để xác nhận camera hoạt động bình thường.

3. Biên dịch chương trình V4L2

Sử dụng Makefile dưới đây để biên dịch mã nguồn. Lưu ý thay đổi định dạng pixel (ví dụ V4L2_PIX_FMT_YUYV) theo đúng định dạng camera hỗ trợ.

# Makefile
CC=gcc
CFLAGS= -g -Wall -O2
TARGET= camera.bin
SRCS= capture.c
OBJS= capture.o

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

.PHONY: clean
clean:
	rm -rf *.o $(TARGET) output.*

4. Mã nguồn chính (capture.c)

Chương trình sử dụng V4L2 để mở thiết bị /dev/video0, cấu hình độ phân giải 640x480, định dạng YUYV, và ghi 100 khung hình ra file out.yuv.

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <linux/videodev2.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <string.h>
#include <assert.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <asm/types.h>

#define CLEAN(x) (memset(&(x), 0, sizeof(x)))
#define WIDTH 640
#define HEIGHT 480

typedef struct {
    void *start;
    unsigned int length;
} FrameBuffer;

int fd;
int out_fd;
int frame_size;
FrameBuffer *buffers = NULL;

int safe_ioctl(int fd, int request, void *arg) {
    int ret;
    do {
        ret = ioctl(fd, request, arg);
    } while (ret == -1 && errno == EINTR);
    return ret;
}

void exit_error(const char *msg) {
    perror(msg);
    exit(EXIT_FAILURE);
}

int open_device(const char *dev) {
    struct stat st;
    if (stat(dev, &st) == -1)
        exit_error("Cannot identify device");
    if (!S_ISCHR(st.st_mode))
        exit_error("Not a character device");
    fd = open(dev, O_RDWR | O_NONBLOCK, 0);
    if (fd == -1)
        exit_error("Cannot open device");
    return 0;
}

int init_device() {
    struct v4l2_capability cap;
    if (safe_ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1)
        exit_error("VIDIOC_QUERYCAP");
    printf("Driver: %s\nCard: %s\n", cap.driver, cap.card);

    struct v4l2_format fmt;
    CLEAN(fmt);
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = WIDTH;
    fmt.fmt.pix.height = HEIGHT;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    if (safe_ioctl(fd, VIDIOC_S_FMT, &fmt) == -1)
        exit_error("VIDIOC_S_FMT");

    if (safe_ioctl(fd, VIDIOC_G_FMT, &fmt) == -1)
        exit_error("VIDIOC_G_FMT");
    printf("Width: %d, Height: %d, Format: %c%c%c%c\n",
           fmt.fmt.pix.width, fmt.fmt.pix.height,
           fmt.fmt.pix.pixelformat & 0xFF,
           (fmt.fmt.pix.pixelformat >> 8) & 0xFF,
           (fmt.fmt.pix.pixelformat >> 16) & 0xFF,
           (fmt.fmt.pix.pixelformat >> 24) & 0xFF);

    frame_size = fmt.fmt.pix.sizeimage;
    return 0;
}

int init_mmap() {
    struct v4l2_requestbuffers req;
    CLEAN(req);
    req.count = 4;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    if (safe_ioctl(fd, VIDIOC_REQBUFS, &req) == -1)
        exit_error("VIDIOC_REQBUFS");

    buffers = calloc(req.count, sizeof(FrameBuffer));
    for (int i = 0; i < req.count; i++) {
        struct v4l2_buffer buf;
        CLEAN(buf);
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;
        if (safe_ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1)
            exit_error("VIDIOC_QUERYBUF");

        buffers[i].length = buf.length;
        buffers[i].start = mmap(NULL, buf.length,
                                PROT_READ | PROT_WRITE,
                                MAP_SHARED, fd, buf.m.offset);
        if (buffers[i].start == MAP_FAILED)
            exit_error("mmap");

        if (safe_ioctl(fd, VIDIOC_QBUF, &buf) == -1)
            exit_error("VIDIOC_QBUF");
    }
    return 0;
}

void start_stream() {
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (safe_ioctl(fd, VIDIOC_STREAMON, &type) == -1)
        exit_error("VIDIOC_STREAMON");
}

void read_frame() {
    struct v4l2_buffer buf;
    CLEAN(buf);
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;

    if (safe_ioctl(fd, VIDIOC_DQBUF, &buf) == -1)
        exit_error("VIDIOC_DQBUF");

    write(out_fd, buffers[buf.index].start, frame_size);

    if (safe_ioctl(fd, VIDIOC_QBUF, &buf) == -1)
        exit_error("VIDIOC_QBUF");
}

void cleanup() {
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    safe_ioctl(fd, VIDIOC_STREAMOFF, &type);
    for (int i = 0; i < 4; i++)
        munmap(buffers[i].start, buffers[i].length);
    free(buffers);
    close(fd);
    close(out_fd);
}

int main() {
    open_device("/dev/video0");
    out_fd = open("out.yuv", O_RDWR | O_CREAT, 0777);
    init_device();
    init_mmap();
    start_stream();

    for (int i = 0; i < 100; i++) {
        read_frame();
        printf("Frame %d captured\n", i);
    }

    cleanup();
    return 0;
}

5. Xem file YUV

File out.yuv thu được có thể xem bằng các công cụ như RawPlayer hoặc YUVPlayer (có sẵn trên GitHub). Các công cụ này hỗ trợ nhiều định dạng YUV và RGB.

Để tích hợp V4L2 với FFmpeg, tham khảo mã nguồn mẫu dưới đây sử dụng thư viện FFmpeg và SDL để thu thập và hiển thị video trực tiếp.

#include <stdio.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavdevice/avdevice.h>
#include <libavutil/imgutils.h>
#include <SDL2/SDL.h>
#include <pthread.h>

#define VIDEO_DEV "/dev/video2"

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int w, h, img_size;
unsigned char *rgb_buffer;
volatile int running = 1;

void *capture_thread(void *arg) {
    avdevice_register_all();
    AVInputFormat *fmt = av_find_input_format("video4linux2");
    AVFormatContext *ctx = NULL;
    if (avformat_open_input(&ctx, VIDEO_DEV, fmt, NULL) != 0) {
        running = 0;
        return NULL;
    }
    avformat_find_stream_info(ctx, NULL);

    int video_idx = av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    AVCodecContext *codec_ctx = ctx->streams[video_idx]->codec;
    AVCodec *codec = avcodec_find_decoder(codec_ctx->codec_id);
    avcodec_open2(codec_ctx, codec, NULL);

    w = codec_ctx->width;
    h = codec_ctx->height;
    img_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, w, h, 16);
    unsigned char *buff = av_malloc(img_size);
    rgb_buffer = malloc(img_size);

    AVFrame *frame = av_frame_alloc();
    AVFrame *yuv_frame = av_frame_alloc();
    av_image_fill_arrays(yuv_frame->data, yuv_frame->linesize, buff,
                         AV_PIX_FMT_YUV420P, w, h, 16);

    struct SwsContext *sws = sws_getContext(w, h, codec_ctx->pix_fmt,
                                            w, h, AV_PIX_FMT_YUV420P,
                                            SWS_BICUBIC, NULL, NULL, NULL);
    AVPacket pkt;
    int got_frame;
    while (av_read_frame(ctx, &pkt) >= 0 && running) {
        if (pkt.stream_index == video_idx) {
            avcodec_decode_video2(codec_ctx, frame, &got_frame, &pkt);
            if (got_frame) {
                sws_scale(sws, (const uint8_t * const *)frame->data,
                          frame->linesize, 0, h,
                          yuv_frame->data, yuv_frame->linesize);
                pthread_mutex_lock(&mtx);
                memcpy(rgb_buffer, buff, img_size);
                pthread_cond_broadcast(&cond);
                pthread_mutex_unlock(&mtx);
            }
        }
        av_packet_unref(&pkt);
    }
    sws_freeContext(sws);
    av_frame_free(&frame);
    av_frame_free(&yuv_frame);
    av_free(buff);
    avformat_close_input(&ctx);
    running = 0;
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, capture_thread, NULL);
    pthread_detach(tid);

    SDL_Init(SDL_INIT_VIDEO);
    SDL_Window *win = SDL_CreateWindow("Camera", SDL_WINDOWPOS_CENTERED,
                                       SDL_WINDOWPOS_CENTERED, 800, 480,
                                       SDL_WINDOW_RESIZABLE);
    SDL_Renderer *ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED);
    SDL_Texture *tex = SDL_CreateTexture(ren, SDL_PIXELFORMAT_IYUV,
                                         SDL_TEXTUREACCESS_STREAMING, w, h);

    SDL_Event e;
    while (running) {
        while (SDL_PollEvent(&e)) {
            if (e.type == SDL_QUIT) running = 0;
        }
        pthread_mutex_lock(&mtx);
        pthread_cond_wait(&cond, &mtx);
        SDL_UpdateTexture(tex, NULL, rgb_buffer, w);
        pthread_mutex_unlock(&mtx);
        SDL_RenderCopy(ren, tex, NULL, NULL);
        SDL_RenderPresent(ren);
    }

    SDL_DestroyTexture(tex);
    SDL_DestroyRenderer(ren);
    SDL_DestroyWindow(win);
    SDL_Quit();
    free(rgb_buffer);
    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cond);
    return 0;
}

6. Tài liệu tham khảo

Thẻ: V4L2 Ubuntu camera linux ffmpeg

Đăng vào ngày 20 tháng 7 lúc 20:40