Ubuntu で libav を使ってみる

ここでいう libav とは,FFmpeg が提供する libavcodec(符号化・復号),libavformat(コンテナの入出力),libswscale(画素形式の変換・拡大縮小),libavutil(共通機能)などのライブラリの総称である。これらを使って,動画ファイルを読み込み,フレームを復号し,画像ファイルとして保存するプログラムを作る手順を示す。

開発用パッケージのインストール

FFmpeg のライブラリを使うプログラムをビルドするには,ヘッダファイルを含む開発用パッケージが必要である。インストールするには,端末で,次のコマンドを実行する。

sudo apt update
sudo apt -y install build-essential pkg-config ffmpeg \
  libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libsdl2-dev

libsdl2-dev は,復号したフレームを画面に表示するプログラムで使用する SDL2 の開発用パッケージである。インストールされている FFmpeg のバージョンを確認するには,端末で,次のコマンドを実行する。

ffmpeg -version
pkg-config --modversion libavcodec libavformat libswscale

API に関する注意

Web 上には,dranger.com の "An ffmpeg and SDL Tutorial"(tutorial01.c, tutorial02.c など)をはじめとする古い解説が多数ある。しかし,これらが使用している API の多くは FFmpeg 5.0 で削除済みであり,Ubuntu 22.04 (FFmpeg 4.4) 以降,特に Ubuntu 24.04 (FFmpeg 6.1) ではそのままではコンパイルできない。主な対応関係は次の通りである。

動画のフレームを PPM ファイルに保存するプログラム

  1. プログラムの作成

    テキストエディタで decode_ppm.c という名前のファイルを作成し,次の内容を保存する。動画ファイルの先頭から 5 フレームを復号し,RGB24 に変換して frame1.ppmframe5.ppm に保存するプログラムである。

    #include <stdio.h>
    #include <libavcodec/avcodec.h>
    #include <libavformat/avformat.h>
    #include <libswscale/swscale.h>
    
    #define MAX_FRAMES 5
    
    static void save_frame(const AVFrame *frame, int width, int height, int index)
    {
        char filename[64];
        snprintf(filename, sizeof(filename), "frame%d.ppm", index);
        FILE *fp = fopen(filename, "wb");
        if (fp == NULL) return;
        fprintf(fp, "P6\n%d %d\n255\n", width, height);
        for (int y = 0; y < height; y++) {
            fwrite(frame->data[0] + y * frame->linesize[0], 1, width * 3, fp);
        }
        fclose(fp);
    }
    
    int main(int argc, char *argv[])
    {
        if (argc < 2) {
            fprintf(stderr, "usage: %s <video file>\n", argv[0]);
            return 1;
        }
    
        /* コンテナを開き,ストリーム情報を取得 */
        AVFormatContext *fmt_ctx = NULL;
        if (avformat_open_input(&fmt_ctx, argv[1], NULL, NULL) < 0) return 1;
        if (avformat_find_stream_info(fmt_ctx, NULL) < 0) return 1;
        av_dump_format(fmt_ctx, 0, argv[1], 0);
    
        /* ビデオストリームとデコーダを選択 */
        const AVCodec *codec = NULL;
        int video_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
        if (video_index < 0) return 1;
    
        /* AVCodecParameters から AVCodecContext を作り,デコーダを開く */
        AVCodecContext *dec_ctx = avcodec_alloc_context3(codec);
        avcodec_parameters_to_context(dec_ctx, fmt_ctx->streams[video_index]->codecpar);
        if (avcodec_open2(dec_ctx, codec, NULL) < 0) return 1;
    
        AVFrame *frame = av_frame_alloc();
        AVFrame *rgb = av_frame_alloc();
        rgb->format = AV_PIX_FMT_RGB24;
        rgb->width  = dec_ctx->width;
        rgb->height = dec_ctx->height;
        av_frame_get_buffer(rgb, 0);
    
        struct SwsContext *sws_ctx = sws_getContext(
            dec_ctx->width, dec_ctx->height, dec_ctx->pix_fmt,
            dec_ctx->width, dec_ctx->height, AV_PIX_FMT_RGB24,
            SWS_BILINEAR, NULL, NULL, NULL);
    
        AVPacket *pkt = av_packet_alloc();
        int saved = 0;
        while (saved < MAX_FRAMES && av_read_frame(fmt_ctx, pkt) >= 0) {
            if (pkt->stream_index == video_index && avcodec_send_packet(dec_ctx, pkt) == 0) {
                while (saved < MAX_FRAMES && avcodec_receive_frame(dec_ctx, frame) == 0) {
                    sws_scale(sws_ctx,
                              (const uint8_t * const *)frame->data, frame->linesize,
                              0, dec_ctx->height, rgb->data, rgb->linesize);
                    save_frame(rgb, dec_ctx->width, dec_ctx->height, ++saved);
                }
            }
            av_packet_unref(pkt);
        }
    
        av_packet_free(&pkt);
        sws_freeContext(sws_ctx);
        av_frame_free(&rgb);
        av_frame_free(&frame);
        avcodec_free_context(&dec_ctx);
        avformat_close_input(&fmt_ctx);
        printf("%d frames saved\n", saved);
        return 0;
    }
    
  2. コンパイル

    端末で,次のコマンドを実行する。pkg-config を使うことで,ヘッダファイルとライブラリの指定が自動的に行われる。

    gcc -o decode_ppm decode_ppm.c $(pkg-config --cflags --libs libavformat libavcodec libswscale libavutil)
    
  3. 実行

    動画ファイルを用意し,端末で,次のコマンドを実行する。動画ファイルが手元にない場合は,1 行目のコマンドで FFmpeg にテスト用の動画を生成させることができる。

    ffmpeg -f lavfi -i testsrc=size=640x480:rate=30:duration=5 -pix_fmt yuv420p test.mp4
    ./decode_ppm test.mp4
    

    実行すると,frame1.ppm, frame2.ppm, frame3.ppm, frame4.ppm, frame5.ppm ができる。生成された画像を確認するには,端末で,次のコマンドを実行する。

    ls -l frame*.ppm
    xdg-open frame1.ppm
    

画面表示を行うプログラム

復号したフレームを画面に表示するには,SDL2 のテクスチャに YUV データを転送する。音声との同期を含む動画プレイヤーの実装例は,FFmpeg の新しい API に対応した次のリポジトリで公開されている。

SDL2 を使うプログラムをコンパイルするときは,リンクの指定に SDL2 を追加する。端末で,次のコマンドを実行する。

gcc -o player player.c $(pkg-config --cflags --libs libavformat libavcodec libswscale libavutil sdl2)
FFmpeg 本体(コマンド ffmpeg)のインストール手順はこちらの別ページで説明する。