📹

V4L2でカメラ映像をキャプチャしてみた

に公開

仕事でV4L2を触れる機会はそれなりにあるのですが、gstreamerだったりから間接的に触ることが多く、「ちゃんと勉強しよう!」と思い立ったので、調べた内容をアウトプットとして残していきたいとおもいます。
まずは手始めに手元にあったUVCカメラを用いてカメラからの映像をキャプチャするところまでをまとめておこうと思います。

映像キャプチャまでのフロー

ドキュメントにおいてはV4L2デバイス(※V4L2に対応したカメラ等のハードウェア)を操作するためのプログラミングは以下の手順で構成されていると記載されています。

  1. デバイスを開く(Opening the device)
  2. デバイスプロパティの変更(Changing device properties, selecting a video and audio input, video standard, picture brightness a. o.)
  3. データフォーマットの交渉(Negotiating a data format)
  4. 入出力方式の交渉とバッファの確保(Negotiating an input/output method)
  5. 入出力ループ(The actual input/output method)
  6. デバイスを閉じる(Closing the device)

2の手順においてselecting a video and audio input, video standardと記載されているようにアナログ映像に関するビデオ規格や、複数入力が存在するビデオキャプチャカードを前提とした記述がされています。これはV4L2の前身のV4Lがアナログテレビおよびラジオデバイスの統一的なインターフェースとして導入された背景があり、歴史的経緯が色濃く残っているのだと思います。しかしながら、今回に関してはUVCカメラを用いることを前提としておりますので入力はセンサからの1入力かつデジタルになりますので2のステップはスキップします。輝度(brightness)についてはUVCカメラでも調整可能になりますが(※カメラによる)、今回についてはGUIでスライダーで輝度調整を行うような実装を行うわけでもありませんので、特段設定は行わないことといたします。

1. デバイスを開く

V4L2 open()

open(2)でオープンしたいデバイスファイル名(例: /dev/video0)を指定してファイルディスクリプタを取得します。

Synopsis

#include <fcntl.h>
int open(const char *device_name, int flags)

Arguments

  • device_name: オープンするデバイスのデバイスファイル名
  • flag: オープン時に指定するオプション

Return Value

  • 成功時: オープンしたファイルのファイルディスクリプタを返す。
  • 失敗時: -1 (具体的なエラー原因はerrno変数にセットされます)

Description

flagにはO_RDWR(読み書き用)フラグの指定が必須になります。これはあくまでv4l2の技術的な都合で、入力デバイスは読み取りのみ、出力デバイスは書き込みのみをサポートします。またO_NONBLOCKフラグを立てると、read(2)VIDIOC_DQBUF(※後述)を実行したタイミングでキューにデータがない場合でも、ブロックせずに即座にEAGAINを返すことができます。

https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/func-open.html
https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/media/v4l/func-open.rst

V4L2 ioctl()

open(2)でデバイスを開いたら以降の手順については、ioctl(2)というスペシャルファイル(キャラクタデバイスなど)に対してread(2)write(2)では扱えない、デバイスのパラメータを操作するためのシステムコールであるioctl(2)を用いることになりますので、まずはV4L2におけるioctl(2)の利用方法について簡単に説明しておきます。

Synopsis

#include <sys/ioctl.h>

int ioctl(int fd, int request, void *argp)

Arguments

  • fd: オープンされたファイルディスクリプタ
  • request: videodev2.hに定義されたV4L2 ioctlリクエストコード
  • argp: 関数パラメータ(通常は構造体)へのポインタ ※操作によってはintへのポインタを渡すこともある

Return Value

  • 成功時: 0
  • 失敗時: -1 (具体的なエラー原因はerrno変数にセットされます)

Description

基本的にやりたい操作のリクエストコードと操作対象の構造体へのポインタを与えてドライバに構造体のフィールドを埋めてもらいデバイスに関する情報をもらう、あるいはアプリケーション側でデバイスを所望の設定にするために構造体のフィールドを埋めてドライバ側に渡す、というようなかたちでV4L2を介してデバイスとやりとりするイメージになります。

https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/func-ioctl.html
https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/media/v4l/func-ioctl.rst

ioctl VIDIOC_QUERYCAP

まずはopen(2)で開いたデバイスに関する情報を取得する必要があります。ハードウェアとドライバに関する情報を取得するAPIは'VIDIOC_QUERYCAP' ioctlになります。

int ioctl(int fd, VIDIOC_QUERYCAP, struct v4l2_capability *argp)

第3引数に渡しているv4l2_capability構造体にドライバがハードウェアおよびドライバに関する情報を埋めてくれます。v4l2_capability構造体の詳細についてこちらをご確認ください。ここでは特に重要な部分のみピックアップして解説します。

__u32 .capabilities

物理デバイス全体で利用可能な機能についてのすべてのフラグ(Device Capabilities Flags)が設定されています。これは何を言っているかというと、1つのデバイスを接続した場合でも複数のデバイスファイルがエクスポートされる場合があります。例えば、いま手元のUVCカメラ(Logitech StreamCam)を接続してみると以下のように、2つのデバイスファイルがエクスポートされます。

$ ls /dev/video*
/dev/video0  /dev/video1

このように1つのデバイスで複数の機能を持ち合わせたデバイスについては複数のデバイスファイルがエクスポートされることがあります。capabilitiesはこのデバイスで利用可能なすべての機能について網羅しているということになります。

補足:

device Capabilities Flagsはvideodev2.hに定数マクロとしてdefineされています。
https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h#L480

__u32 .device_caps

capabilitiesとは対照的にdevice_capsはオープンされたデバイスの機能のみのフラグ(Device Capabilities Flags)がセットされています。
例えば、先ほど例示したLogitech StreamCam/dev/video0/dev/video1device_capsを確認してみると以下のようになっています。

$ v4l2-ctl -d /dev/video0 --list-formats-ext --all
Driver Info:
        Driver name      : uvcvideo
        Card type        : Logitech StreamCam
        Bus info         : usb-vhci_hcd.0-1
        Driver version   : 6.6.87
        Capabilities     : 0x84a00001
                Video Capture
                Metadata Capture
                Streaming
                Extended Pix Format
                Device Capabilities
        Device Caps      : 0x04200001
                Video Capture
                Streaming
                Extended Pix Format
(以下略)

/dev/video0device Caps0x04200001となっています。
設定されているフラグは以下になります。

名前 フラグ 概要
V4L2_CAP_STREAMING 0x04000000 ストリーミングI/Oをサポート
V4L2_CAP_EXT_PIX_FORMAT 0x00200000 v4l2_pix_format構造体の拡張フィールドに対応
V4L2_CAP_VIDEO_CAPTURE 0x00000001 シングルプレナー形式でのビデオキャプチャをサポート

次に/dev/video1を確認してみます。

$ v4l2-ctl -d /dev/video1 --list-formats-ext --all
Driver Info:
        Driver name      : uvcvideo
        Card type        : Logitech StreamCam
        Bus info         : usb-vhci_hcd.0-1
        Driver version   : 6.6.87
        Capabilities     : 0x84a00001
                Video Capture
                Metadata Capture
                Streaming
                Extended Pix Format
                Device Capabilities
        Device Caps      : 0x04a00000
                Metadata Capture
                Streaming
                Extended Pix Format
(以下略)

/dev/video1device_caps0x04a00000でした。
設定されているフラグは以下になります。

名前 フラグ 概要
V4L2_CAP_STREAMING 0x04000000 ストリーミングI/Oをサポート
V4L2_CAP_META_CAPTURE 0x00800000 メタデータキャプチャインターフェースをサポート
V4L2_CAP_EXT_PIX_FORMAT 0x00200000 v4l2_pix_format構造体の拡張フィールドに対応

/dev/video0はビデオキャプチャに対応し、/dev/video1はキャプチャする映像に関するメタデータ(露出設定など)のキャプチャに対応していることが分かります。

以上の内容を踏まえて、ハードウェアおよびドライバに関する情報を取得する(=映像のキャプチャが可能かを確認する)するコードはこんな感じになります。

int check_device_capabilities(int fd, struct v4l2_capability *caps)
{
    uint32_t cap;

    if (ioctl(fd, VIDIOC_QUERYCAP, caps) == -1) {
        perror("VIDIOC_QUERYCAP");
        return -1;
    }

    if (caps->capabilities & V4L2_CAP_DEVICE_CAPS) {
        /* device_caps(ノード固有の機能)を優先使用 */
        cap = caps->device_caps;
    } else {
        /* capabilities(物理デバイス全体の機能)を使用 */
        cap = caps->capabilities;
    }

    /* キャプチャデバイスであるかを確認 */
    if (!(cap & (V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_VIDEO_CAPTURE_MPLANE))) {
        fprintf(stderr, "This device is not a video capture device.\n");
        return -1;
    }

    /* ストリーミングに対応しているか確認 */
    if (!(cap & V4L2_CAP_STREAMING)) {
        fprintf(stderr, "This device does not support streaming I/O.\n");
        return -1;
    }

    return 0;
}

V4L2_CAP_DEVICE_CAPSフラグとv4l2_capability構造体の.capabilitiesで論理積を取っている条件式があります。こちらの処理が必要になる理由は、v4l2_capability構造体の.device_capsLinux 3.3で追加され、それ以前のバージョンに対応した古いドライバでは対応していないからです。Linux 3.3以降のV4L2に対応したドライバの場合、.capabilitiesV4L2_CAP_DEVICE_CAPSフラグをセットしてくれているので、このように確認する必要があります。以降の処理についてはセットされているフラグのチェックを行い、キャプチャデバイスであるかを確認していく作業になります。V4L2_CAP_VIDEO_CAPTURE_MPLANEフラグは、SoCなどにおいて輝度(Y)と色差(UV)を別々の回路で処理するなどの都合上、別々のメモリ領域にデータを格納することを要求するハードウェアが存在し、これを識別するためのフラグになります。またUVCカメラの場合は、シングルプレーンな1つの連続したメモリ領域に画像データが格納されます(識別子はV4L2_CAP_VIDEO_CAPTUREフラグ)。V4L2_CAP_STREAMINGフラグはV4L2のStreaming I/Oに対応しているかを識別するフラグになります。Streaming I/Oについては後段のチャプターで説明します。

2. デバイスプロパティの変更

今回はスキップ

3. データフォーマットの交渉

解像度、ピクセルフォーマットなどのカメラパラメータについてネゴシエーションを行います。
以下で説明するVIDIOC_G_FMT, VIDIOC_S_FMT, VIDIOC_TRY_FMTリクエストコードを用いて、現在の設定の確認(VIDIOC_G_FMT)、パラメータの設定(VIDIOC_S_FMT)、設定の試行(VIDIOC_TRY_FMT)を行うことが出来ます。

ioctl VIDIOC_G_FMT

int ioctl(int fd, VIDIOC_G_FMT, struct v4l2_format *argp)
  • Gは恐らくGETGかと。
  • 現在のパラメータを確認するにはv4l2_format構造体に適切なバッファタイプを指定する。
    • VIDIOC_QUERYCAP ioctl で、V4L2_CAP_VIDEO_CAPTUREフラグがセットされていればV4L2_BUF_TYPE_VIDEO_CAPTURE
    • VIDIOC_QUERYCAP ioctl で、V4L2_CAP_VIDEO_CAPTURE_MPLANEフラグがセットされていればV4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
  • VIDIOC_G_FMT ioctlを呼び出すと、v4l2_format構造体の.fmtが共用体になっており、.typeで指定されたバッファタイプに合わせた、共用体のメンバ(構造体)にドライバが書き込みを行ってくれる。
    • ビデオキャプチャデバイスの場合は.fmt.pix(v4l2_pix_fmt構造体)または.fmt.pix_mp(v4l2_pix_format_mplane構造体)のいずれか。

v4l2_format構造体およびfmtメンバの共用体メンバの構造体の詳細についてはこちらをご確認ください。

ioctl VIDIOC_S_FMT

int ioctl(int fd, VIDIOC_S_FMT, struct v4l2_format *argp)
  • Sは恐らくSETSかと。
  • パラメータを変更するには、fmtフィールドの共用体メンバを初期化する必要がある。
    • ビデオキャプチャの場合は、v4l2_pix_format構造体あるいはv4l2_pix_format_mplane構造体
    • 変更が必要なパラメータを変更する。
  • VIDIOC_S_FMT ioctlが呼び出されると、ドライバはデバイスの機能を確認し、アプリケーションが要求したパラメータにハードウェアが対応していなかった場合、それに近しい値をセットする。

ioctl VIDIOC_TRY_FMT

int ioctl(int fd, VIDIOC_TRY_FMT, struct v4l2_format *argp)
  • VIDIOC_TRY_FMTVIDIOC_S_FMTと同等の機能であるが、ドライバの状態を変更しないという点が異なる。
  • いつでも呼び出すことが出来て、EBUSYを返すことはない。
  • I/Oを中断したり時間のかかるハードウェアの準備をせずに、パラメータのネゴシエーションやハードウェアのリミテーションを把握するために提供されている。

以下に解像度およびピクセルフォーマットのネゴシエーションを行うサンプルコードを示します。

int negotiate_video_fmt(int fd, struct v4l2_format *fmt, uint32_t width, uint_32_t height, uint32_t pixelformat)
{
    memset(fmt, 0, sizeof(*fmt));
    fmt->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt->fmt.pix.width = width;
    fmt->fmt.pix.height = height;
    fmt->fmt.pix.pixelformat = pixelformat;
    fmt->fmt.pix.field = V4L2_FIELD_NONE;
    
    if (ioctl(fd, VIDIOC_S_FMT, fmt) == -1) {
        perror("VIDIOC_S_FMT");
        return -1;
    }
    
    /* ドライバが実際に設定した値を確認する */
    if (fmt->fmt.pix.width != width || fmt->fmt.pix.height != height) {
        fprintf(stderr, "Warning: Requested resolution %ux%u, but driver set %ux%u\n",
                width, height, fmt->fmt.pix.width, fmt->fmt.pix.height);
    }
    if (fmt->fmt.pix.pixelformat != pixelformat) {
        fprintf(stderr, "Warning: Requested pixelformat 0x%08x, but driver set 0x%08x\n", pixelformat, fmt->fmt.pix.pixelformat);
    }
    
    return 0;
}

簡単にコードについて解説しますと、

  • 今回使用しているカメラ(Logitech StreamCam)は、ピクセルフォーマットとしてYUYVMJPEGNV12をサポートしており、かつVIDIOC_QUERYCAP ioctlにて/dev/video0V4L2_CAP_VIDEO_CAPTUREをサポートしているため、v4l2_format構造体の.typeにはV4L2_BUF_TYPE_VIDEO_CAPTUREのバッファタイプを指定。
    • サポートしているピクセルフォーマットの確認は(VIDIOC_ENUM_FMT ioctl、あるいはv4l2-ctlコマンドで確認できる)
  • V4L2_BUF_TYPE_VIDEO_CAPTUREのバッファタイプなのでアクセスする共用体メンバは.fmt.pix(v4l2_pix_fmt構造体)になる。
    • .fmt.pix.width, .fmt.pix.heightで解像度を変更
    • .fmt.pix.pixelformatでピクセルフォーマットをYUYV(V4L2_PIX_FMT_YUYV)に変更
      • 指定するコードはlinux/videodev2.hに定数マクロとして定義されている。
      • 指定可能なコードの一覧はここで確認できる。
    • .fmt.pix.fieldにはインターレースにおけるフィールドオーダー(トップとボトムどちらを先に再生するか)のタイプを指定している。
      • プログレッシブの場合は、V4L2_FIELD_NONEを指定。

今回は解像度およびピクセルフォーマットの変更しか行っておりませんが、v4l2_pix_format構造体を見る感じ、色空間の変更等も出来るようです。

https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-g-fmt.html
https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/media/v4l/vidioc-g-fmt.rst

入出力方式の交渉とバッファの確保

今回はStreaming I/Oというデータ入出力方式を利用します。このStreaming I/OVIDIOC_QUERYCAP ioctlで返されるv4l2_capability構造体の.capabilitiesV4L2_CAP_STREAMINGフラグがセットされている場合に利用できます(今回使用するLogitech StreamCamは、1. デバイスを開くで確認したようにStreaming I/Oをサポートしています)。Streaming I/Oはゼロコピーで高速にデータ転送を行うことができる入出力方式になります。通常、ファイルの読み込み(read(2))を行う場合、カーネル空間のメモリ領域に格納されているデータをユーザー空間のメモリ領域にコピーを行います。しかし、画像データという巨大なデータを秒間30~60枚逐次コピーを行っているとCPU負荷が高くなりすぎてしまうため、アプリケーションとドライバ間でバッファへのポインタの交換のみを行い、ゼロコピーでの高速なデータ転送を実現しています。Streaming I/Oには以下の3つのデータ入出力方式をサポートしています。

  1. Memory Mapping(V4L2_MEMORY_MMAP)
    ドライバがデバイスメモリまたはカーネルメモリ上にバッファを確保し、アプリケーションがそれを自身のアドレス空間にマッピング(mmap(2))してアクセスする方法。
  2. User Pointer(V4L2_MEMORY_USERPTR)
    アプリケーション側で確保した仮想メモリのポインタをドライバに渡して使用する方法。
  3. DMA Buffer(V4L2_MEMORY_DMABUF)
    バッファに紐づけられたファイルディスクリプタを介して、デバイス間(V4L2デバイス⇔GPUなど)でバッファを共有する方法。

今回は、1のメモリマッピングを利用します。
手順としては以下のような流れになります。

  1. ドライバに対してバッファのアロケーションリクエストを送る(VIDIOC_REQBUFS)
    →ドライバ内部でバッファが確保されます。
  2. 確保されたバッファの情報を問い合わせ(VIDIOC_QUERYBUF)
    →各バッファのサイズや、マッピングに必要なオフセットを取得
  3. ユーザー空間へのマッピングを行う(mmap(2))
    →取得したオフセットを用いて、アプリケーションからアクセス可能なアドレスを取得します。

ioctl VIDIOC_REQBUFS

int ioctl(int fd, VIDIOC_REQBUFS, struct v4l2_requestbuffers *argp)
  • v4l2_requestbuffers構造体の以下のメンバに値をセットし、ドライバにメモリ確保のリクエストとI/O方式について通知します。
    • .count: バッファリングするフレーム数
      • (アプリ側): 何フレーム分のバッファが欲しいかを書き込んで渡す。
      • (ドライバ側): 確保できた実際のフレーム数に書き換えて返す。
    • .type: バッファの種類
      • (アプリ側): Logitech StreamCamではV4L2_BUF_TYPE_VIDEO_CAPTUREを指定
      • (ドライバ側): 指定されたタイプがサポートされているかチェック
    • .memory: I/O方式
      • (アプリ側): 今回はV4L2_MEMORY_MMAPを指定
      • (ドライバ側): 指定された入出力方式に切り替えを行う

サンプルコードを以下に示します。

int allocate_device_buffer(int fd, struct v4l2_requestbuffers *req, uint32_t num_buffers)
{
    memset(req, 0, sizeof(*req)); /* reservedフィールドはゼロクリアがマスト */
    req->count = num_buffers;
    req->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req->memory = V4L2_MEMORY_MMAP;

    /* バッファの要求 */
    if (ioctl(fd, VIDIOC_REQBUFS, req) == -1) {
        if (errno == EINVAL) {
            fprintf(stderr, "Device does not support memory mapping\n");
        } else {
            perror("VIDIOC_REQBUFS");
        }
        return -1;
    }
    if (req->count < num_buffers) {
        fprintf(stderr, "Insufficient buffer memory on device\n");
        return -1;
    }

    return 0;
}

補足としてはif (req->count < num_buffers){...}の部分で、要求したバッファ数と実際に割り当てられたバッファ数を比較し、要求したバッファ数よりドライバによって割り当てられたバッファ数が小さい場合はエラーとして扱っています。これはドライバ側のメモリ不足により要求された数よりも小さい数が割り当てられることがあるため(場合によっては0になることも)、必ず確認する必要があります(エラーにするかは別として)。またv4l2_requestbuffers構造体の.countに0をセットしてVIDIOC_REQBUFSを呼び出すと、確保されたバッファを解放し、ストリーミングを停止する効果があります。これはアプリケーションの終了処理や、解像度変更時の再確保で重要になります。

https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-reqbufs.html
https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/media/v4l/vidioc-reqbufs.rst

ioctl VIDIOC_QUERYBUF

VIDIOC_REQBUFS ioctlを実行すると、バッファはカーネル空間のメモリ領域に割り当てられますが、アプリケーション側はその詳細を把握出来ていません。したがってVIDIOC_QUERYBUFではバッファからmmap(2)するために必要なオフセットサイズを取得します。

int ioctl(int fd, VIDIOC_QUERYBUF, struct v4l2_buffer *argp)

以下にVIDIOC_QUERYBUF ioctl実行前にv4l2_buffer構造体に設定すべき事項について記載します。

  • アプリケーションはv4l2_format構造体およびv4l2_requestbuffers構造体のtypeメンバにセットしたバッファタイプをv4l2_buffer構造体のtypeメンバに設定する。
  • v4l2_buffer構造体のindexメンバにはバッファに割り振られているインデックスを指定します。バッファにはVIDIOC_REQBUFS ioctlで割り当てられたバッファ数までの範囲で(バッファ数が4であれば0~3)インデックスが振られており、今回情報を取得するバッファのインデックスを指定します。

なお、.memory(I/O方式)、.length(サイズ)、.m.offset(オフセット)などは、このioctlが成功した後にドライバによって値が埋められます。

https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-querybuf.html
https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/media/v4l/vidioc-querybuf.rst

V4L2 mmap()

VIDIOC_QUERYBUFで取得したバッファの情報をもとにユーザー空間へのマッピングを行います。

#include <unistd.h>
#include <sys/mman.h>

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
  • start:
    バッファをアプリケーションのアドレス空間のこのアドレスにマップする。NULLポインタを渡す(=OSに委ねる)が推奨されている。
  • length:
    マッピングするメモリ領域の長さ(サイズ)。VIDIOC_QUERYBUFで返されたv4l2_buffer構造体の.lengthをそのまま渡す必要がある。(バッファタイプがV4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANEのデバイスは、struct v4l2_plane構造体の.length)
  • prot:
    必要なメモリの保護モードについて指定する。デバイスの種類やデータ交換の方向にかかわらず、PROT_READ | PROT_WRITEに設定し、バッファへの読み取りと書き込みアクセスを許可する必要がある。
  • flags:
    マッピングされたメモリ領域の振る舞いを決めるパラメータ。V4L2においてはMAP_SHAREDを指定する必要がある。
  • fd:
    V4L2デバイスのファイルディスクリプタ
  • offset:
    カーネル空間のバッファのオフセット。VIDIOC_QUERYBUFで返されたv4l2_buffer構造体の.m.offsetをそのまま渡す必要がある。(※バッファタイプがV4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANEのデバイスは.m.planes[i].m.mem_offset)

https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/func-mmap.html
https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/media/v4l/func-mmap.rst?plain=1

以下にVIDIOC_QUERYBUF ioctlでカーネル空間に存在するバッファに関する情報を取得し、ユーザー空間のメモリ領域にマッピングするまでのサンプルコードを例示します。

typedef struct {
    void *start;
    size_t length;
} raw_img_buffer;

int query_status_buffer(int fd, uint32_t n_buffers, raw_img_buffer *buffers, struct v4l2_buffer *buf)
{
    for (uint32_t i = 0; i < n_buffers; ++i) {
        memset(buf, 0, sizeof(*buf));
        buf->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf->memory = V4L2_MEMORY_MMAP;
        buf->index = i; /* 0から順に問い合わせる */

        /* バッファに関する情報を取得 */
        if (ioctl(fd, VIDIOC_QUERYBUF, buf) == -1) {
            perror("VIDIOC_QUERYBUF");
            return -1;
        }

        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) {
            perror("mmap");
            return -1;
        }
    }

    return 0;
}

5. 入出力ループ

バッファの管理方法について

まずはV4L2におけるバッファ管理について簡単に解説しておこうと思います。
V4L2においては2つのバッファキューが存在します。

  1. 入力キュー(Incoming Queue):
    アプリ側が「これ使っていいよ」とドライバに渡した空のバッファが格納されるキュー。ドライバはここからバッファを取り出し、映像データを書き込むようにハードウェアに指示を出す。
  2. 出力キュー(Outgoing Queue):
    カメラが映像を書き込み終わり、アプリが取りに来るのを待っているキュー。

なぜこのような2つのキューが必要になるかというと、カメラは一定間隔(30fspなら1秒に30回)で容赦なくデータを送り続けてきます。一方でアプリケーション側はディスク書き込みまたはネットワーク遅延や他のプロセスの影響等で処理遅延が発生することがあるため、データを取りこぼさないためにも2つのキューを並ばせておく必要があります。

処理フローは以下のようなイメージになります。

  1. メモリマップされたバッファをすべて入力キューに登録する。
  2. アプリケーションは出力キューからデキュー出来るまで待機し、デキューを行う。
  3. アプリケーション側でデータが不要になったら、バッファを再度入力キューにエンキューする。

バッファが現在どのキューにあるかはv4l2_buffer構造体のflagsメンバで確認できます。なお、以下の2つのフラグは相互に排他的であり、同時にセットされることはありません。

名前 フラグ 概要
V4L2_BUF_FLAG_QUEUED 0x00000002 バッファは入力キューに存在する状態。バッファへの書き込みが完了すると自動的に出力キューに移動する。
V4L2_BUF_FLAG_DONE 0x00000004 バッファは出力キューに存在し、ドライバからデキューされる準備が出来ている状態。

※どちらのフラグも立っていない場合は、バッファはアプリケーションの手元(デキュー状態)にあり、アプリケーションが読み書き可能な状態であることを示します。

ステップ4の入出力方式の交渉とバッファの確保で先述したようにVIDIOC_QUERYBUF ioctlで、上記に記載した各々のバッファの状態を確認することが出来ます。

ioctl VIDIOC_STREAMON

実際の入出力ループに入る前に、VIDIOC_STREAMON ioctlでSTREAMING I/Oによるキャプチャ処理の開始を行わなければいけません。VIDIOC_STREAMONが呼び出されるまでキャプチャハードウェアは無効化され、入力キューに存在するバッファは埋められません。

int ioctl(int fd, VIDIOC_STREAMON, const int *argp)

第3引数のargpには、VIDIOC_REQBUFS ioctlで渡したv4l2_requestbuffers構造体のtypeメンバにセットしたバッファタイプ(enum v4l2_buf_type)[https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/buffer.html#c.V4L.v4l2_buf_type]と同様の値をセット(enum v4l2_buf_type型の値を格納した変数へのポインタを)する必要があります。

https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/media/v4l/vidioc-streamon.rst
https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-streamon.html

ioctl VIDIOC_QBUF

ドライバの入力キューにバッファのエンキューを行う。

int ioctl(int fd, VIDIOC_QBUF, struct v4l2_buffer *argp)

このVIDIOC_QBUF ioctlでは、v4l2_bufferへのポインタを引数にとります。呼び出す前に以下のメンバについて設定を行う必要があります。

  • .type:
    バッファタイプ。VIDIOC_REQBUFS ioctlで指定したものと同じものを指定する必要がある。
  • .memory:
    データの入出力方式(V4L2_MEMORY_MMAP, V4L2_MEMORY_USERPTR, V4L2_MEMORY_DMABUF) ※今回の場合はV4L2_MEMORY_MMAPを指定。
  • .index:
    バッファのインデックス(0~確保したバッファ数-1)

https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-qbuf.html
https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/media/v4l/vidioc-qbuf.rst

ioctl VIDIOC_DQBUF

ドライバの出力キューからバッファのデキューを行う。

int ioctl(int fd, VIDIOC_DQBUF, struct v4l2_buffer *argp)

VIDIOC_QBUFと同様に、v4l2_buffer構造体の.type.memoryの設定を行いioctlを呼び出します。成功すると、ドライバは構造体の残りのフィールドを埋めて返します。ドライバが埋めてくれる情報のうち、特に重要なものについて解説します。

  • .index:
    どのバッファが戻ってきたかを示すインデックス番号。アプリケーションはこの番号を使って、
    mmap(2)したメモリアドレスなどを特定します。
  • .bytesused:
    バッファ内のデータが占めるバイト数。ネゴシエートされたデータフォーマットに依存し、JPEG画像のような圧縮された可変サイズデータの場合は、バッファごとに変化する可能性があります。
  • .timestamp:
    フレームがキャプチャされた正確な時刻。映像と音声の同期や、フレームレートの計測に使用します。
  • .sequence:
    フレームのシーケンス番号。アプリケーションはこの番号が飛んでいるかどうかを見ることで、フレームドロップが発生したかを検知できます。

VIDIOC_DQBUF ioctlが成功するとバッファの所有権がドライバからアプリケーションに戻り、バッファのデータを読み込むことが出来るようになります。また、VIDIOC_DQBUFはデフォルトでは出力キューにバッファが存在しない場合処理をブロックします。ノンブロッキングで動作させるにはopen(2)関数でデバイスを開く際にO_NONBLOCKフラグを設定します。そうするとEAGAINエラーコードを即座に返すようになり、別の処理を行うことが出来ます。

ioctl VIDIOC_STREAMOFF

Streaming I/O(映像のキャプチャや出力)を停止するためのioctlコマンド。

int ioctl(int fd, VIDIOC_STREAMOFF, const int *argp)

第3引数のargpには、VIDIOC_REQBUFS ioctlで渡したv4l2_requestbuffers構造体のtypeメンバにセットしたバッファタイプ(enum v4l2_buf_type)[https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/buffer.html#c.V4L.v4l2_buf_type]と同様の値をセット(enum v4l2_buf_typeの値を格納した変数へのポインタを)する必要があります。(※VIDIOC_STREAMONと同様)

VIDIOC_STREAMOFFではDMA転送が停止され、入力キューおよび出力キューにあるすべてのバッファがキューから取り除かれます。したがってキャプチャ済みでデキューされていないデータは失われることになります。すべてのバッファはVIDIOC_REQBUFS ioctlで確保された直後の状態に戻ります。バッファのメモリ割り当ておよびメモリマッピングを解除していなければ、VIDIOC_QBUF(バッファの再投入)から始め、その後に VIDIOC_STREAMON を呼ぶことでキャプチャを再開することができます。

まとめ

キャプチャまでの処理の流れをシーケンス図に起こしてみると以下のようになります。

サンプルコード

以下は多少雑ではありますがキャプチャしてYUYVで5秒間分ダンプするコードになります。

(2026/01/30)
現状かなり限定的なキャプチャ操作のみの対応にはなりますがライブラリ化しました。(順次機能追加予定)
こちらをご確認ください!
https://github.com/onode-k/Lowlevel-video-lab

Discussion