LoginSignup
37
36

More than 5 years have passed since last update.

FFmpegチュートリアル1: キャプチャする

Last updated at Posted at 2015-01-22

 ソースはgithub!

  • 書き溜めた分が切れました。チュートリアル第2回はいつになるか未定
  • 「データを読み込む」swift版プログラムには、「なんでか分からないけど動いている」部分がいっぱいあります。Voodooが増えるよ! やったねffmpegちゃん!

チュートリアル 01: キャプチャする

概要

 動画ファイルはいくつかの基本的な要素からなります。まずファイルそれ自体は「コンテナー」と呼ばれ、コンテナーの種類によってファイル内部の構造が決まります。例を挙げれば、AVIやQuicktimeなどがコンテナーです。
 その次に「ストリーム」の束があります。たとえば、大抵の場合には音声ストリームと映像ストリームがあります。ストリームというのは、経時的に利用される一連のデータを比喩した表現です。
 各ストリームに含まれるデータを「フレーム」といいます。それぞれのストリームは異なるコーデックによりエンコードされています。コーデックに応じてデータがエンコード (co-de) され、またデコード (dec-ode) されます (co-decという名前はこれに由来します)。DivXやMP3などがコーデックです。
 ストリームから読み取られるのが「パケット」です。パケットはデータのあつまりで、アプリケーションが扱うデコード済みのフレームはここから得られます。今回の場合、それぞれのパケットは完全なフレームを複数持っており、特に音声の場合には大量に持っているでしょう。

 ごくベーシックな部分だけ見れば、映像と音声のストリームは簡単に扱えます。

10 OPEN video_stream FROM video.avi
20 READ packet FROM video_stream INTO frame
30 IF frame NOT COMPLETE GOTO 20
40 DO SOMETHING WITH frame
50 GOTO 20

 ffmpegによってマルチメディアを扱うこと自体は簡単なのですが、往々にして DO SOMETHING の部分が非常に複雑になってしまうのです。今回のチュートリアルでは、ファイルを開き、映像ストリームを読み込み、DO SOMETHING の内容はフレームをPPMファイルに書き込むだけにします。

ファイルを開く

 まずは、ファイルを開く方法を学びましょう。ffmpegを利用するにあたり、まずはライブラリーの初期化が必要です (インクルードするヘッダーは、システム環境によって <ffmpeg/avcodec.h><ffmpeg/avformat.h> になることもあります)。

C言語

#include <avcodec.h>
#include <avformat.h>
...
int main(int argc, charg *argv[]) {
av_register_all();

swift

// ヘッダーは別途 bridging header からインポートする
av_register_all()

 これにより、ライブラリーを通じて利用可能な全てのファイルフォーマットおよびコーデックが登録され、対応フォーマット・コーデックのファイルを開くだけで自動的に適用されます。この操作は一回だけ行えばよいので、main() に記述されています。もし望むなら、特定のフォーマットまたはコーデックのみ登録することも可能ですが、通常そのようにする理由はありません。

 これで、ファイルを開くことができます。

C言語

AVFormatContext *pFormatCtx;

// ファイルを開く
// [翻訳版注記] av_open_input_file()は廃止されています
if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)!=0)
    return -1; // Couldn't open file

swift

// ファイルを開く
var formatContext = UnsafeMutablePointer<AVFormatContext>()
if avformat_open_input(&formatContext, filePath, nil, nil) != 0 {
    println("Couldn't open file")
    return
}

 C言語版では、コマンドライン引数からファイル名を取得しています。この関数を使うことで、ファイルのヘッダーを解析し、ファイルフォーマットについての情報を AVFormatContext 構造体に記録することができます。それ以外の引数を指定すればファイルフォーマットやバッファーサイズ、オプションなどを限定することができますが、NULLを設定すれば libavformat が自動的に検出します。
 しかしこの関数はファイルのヘッダーしか確認しないため、別の関数によってファイル内のストリームに関する情報を得る必要があります。

C言語

// ストリームの情報を得る
// [翻訳版注記] av_find_stream_info()は廃止されています
if(av_find_stream_info(pFormatCtx)<0)
  return -1; // 情報が見つからなかったのでエラー、終了

swift

// ストリームの情報を得る
if avformat_find_stream_info(formatContext, nil) < 0 {
    println("Couldn't find stream information")
    return
}

 この関数は、 pFormatCtx->streams に適切な情報を書き込みます。その内容を確かめるには dump_format 関数が便利です。

C言語

// ファイル情報を標準エラー出力
// [翻訳版注記] dump_formatは廃止されています
dump_format(pFormatCtx, 0, argv[1], 0);

swift

// 入力ファイルのフォーマット情報をstderrに出力する
av_dump_format(formatContext, 0, filePath, false.c)

 この段階では、pFormatCtx->streams はまだポインターの配列でしかありません (その総数は pFormatCtx->nb_streams から分かります)。映像ストリームが見つかるまでループ処理をかけます。

C言語

int i;
AVCodecContext *pCodecCtx;

// 最初の映像ストリームを探す
videoStream=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
  // [翻訳版注記] CODEC_TYPE_VIDEOは廃止されています
  if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO) {
    videoStream=i;
    break;
  }
if(videoStream==-1)
  return -1; // Didn't find a video stream

// 映像ストリームのコーデックコンテキストのポインターを取得する
pCodecCtx=pFormatCtx->streams[videoStream]->codec;

swift

// 最初の映像ストリームを探す
let target = AVMEDIA_TYPE_VIDEO
var avStream: UnsafeMutablePointer<AVStream>? = nil
var n = -1

for i in 0..<Int(formatContext.memory.nb_streams) {
    let s = formatContext.memory.streams[i]
    if s.memory.codec.memory.codec_type.value == target.value {
        avStream = s
        n = i
    }
}
if avStream == nil {
    println("Didn't find a video stream")
    return
}

 コーデックに関するストリームの情報は、「コーデックコンテキスト」にあります。ここにはストリームに利用されているコーデックについての全情報があり、すでにそのポインターも取得しています。しかし更に、実際のコーデックを取得し、これを開く必要があります。

C言語

AVCodec *pCodec;

// 映像ストリームに適したコーデックを探す
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
  fprintf(stderr, "Unsupported codec!\n");
  return -1; // Codec not found
}
// コーデックを開く
// [翻訳版注記] avcodec_open()は廃止されています
if(avcodec_open(pCodecCtx, pCodec)<0)
  return -1; // Could not open codec

swift

// コーデック・コンテキストのポインターを取得
let codecContext = formatContext.memory.streams[n].memory.codec

// 映像ストリームのデコーダーを探す
let codec = avcodec_find_decoder(codecContext.memory.codec_id)
if codec == nil {
    println("Unsupported codec")
    return
}

// コーデックを開く
if avcodec_open2(codecContext, codec, nil) < 0 {
    println("Could not open codec")
    return
}

 以前のチュートリアルでは pCodecCtx->flagsCODEC_FLAG_TRUNCATED を追加したり、極めて不正確なフレームレートを訂正するハックなども紹介していました。これらは最早 ffplay.c に存在しませんし、既に不要だと考えています。これに関連して、 pCodecCtx->time_base がフレームレートに関する情報を持っていることも指摘しておきます。 time_base は分子と分母を持つ構造体です(AVRational)。なぜ分数で表すかといえば、多くのコーデックが非整数のフレームレートを持っているためです。たとえば、NTSCなら29.97fpsといった具合です。

データを保存する

 フレームを保存するためには、そのためのスペースが必要です。

C言語

AVFrame *pFrame;

// 映像フレームにメモリーを割り当てる
// [翻訳版注記] avcodec_alloc_frame()は廃止されています
pFrame=avcodec_alloc_frame();

swift

// 映像フレームのためにメモリーを確保する
var frame = av_frame_alloc()
var convertedFrame = av_frame_alloc()
if frame == nil || convertedFrame == nil {
    return
}

 今回はPPMファイル (24bit RGBのデータを持つ) を出力したいので、フレームをRGBに変換する工程が必要です。このような変換も、ffmpegがあれば可能です。今回ばかりでなく、大抵のプロジェクトではフレームを特定のフォーマットに変換することになるでしょう。ということで、変換済みフレームのためのスペースも用意します。

C言語

// AVFrame構造体にメモリーを割り当てる
// [翻訳版注記] avcodec_alloc_frame()は廃止されています
pFrameRGB=avcodec_alloc_frame();
if(pFrameRGB==NULL)
  return -1;

swift

// もう用意しています

 これでフレームは用意できましたが、これに加えて変換する生のデータを置いておく場所も必要です。avpicture_get_size() によって必要なサイズを求め、それを用意します。

C言語

uint8_t *buffer;
int numBytes;
// 必要なバッファーサイズを調べ、準備する
numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
                            pCodecCtx->height);
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

swift

// 変換後のフレームのために必要なバッファーを確保する
let w = codecContext.memory.width
let h = codecContext.memory.height
let format = PIX_FMT_BGR24
let size = avpicture_get_size(PIX_FMT_BGR24, w, h)
var buffer = [UInt8](count: Int(size), repeatedValue: 0)

 av_malloc はffmpegが用意した malloc であり、メモリーアドレスを調整するなどの機能を持つ単純な malloc ラッパーです。ただし、メモリーリークや二重開放などのメモリー由来の問題まで回避できるものではありません。

[翻訳版注記 開始]
 アップルによるとUnsafePointer<UInt8>[UInt8] は互換性があるらしいです。

定数のポインター:ある関数が UnsafePointer<Type> を引数に取るとき、以下のいずれも用いることができる。

  • nil (ヌルポインター扱いになる)
  • UnsafePointer<Type>, UnsafeMutablePointer<Type>, AutoreleasingUnsafeMutablePointer<Type> のいずれか (自動的に変換される)
  • Type型のLvalueに in-out をつけたもの (参照渡しになる)
  • [Type] (ポインターが渡され、呼び出し中は寿命が保証される)

変数のポインター:ある関数が UnsafeMutablePointer <Type> を引数に取るとき、以下のいずれも用いることができる。

  • nil (ヌルポインター扱いになる)
  • UnsafeMutablePointer<Type>
  • Type型のLvalueに in-out をつけたもの (参照渡しになる)
  • [Type]in-out をつけたもの (ポインターが渡され、呼び出し中は寿命が保証される)

 そういうわけなので、あえてmalloc()を使わずとも、必要なサイズの [UInt8] を用意すればちゃんと動くようです。
[翻訳版注記 終了]

 avpicture_fill を使い、用意したバッファーとフレームを結びつけましょう。なお、AVFrameAVPicture にキャストできるのは、後者が前者のサブセットであるためです (AVFrame の冒頭部分は AVPicture と全く同じです)。

C言語

// バッファーの適当な部分をpFrameRGBの画像に割り当てる
// pFrameRGB は AVFrame であり、 AVFrame は AVPicture のスーパーセットです
avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
                pCodecCtx->width, pCodecCtx->height);

swift

// フレームにバッファーを割り当てる
avpicture_fill(UnsafeMutablePointer<AVPicture>(convertedFrame), UnsafePointer<UInt8>(buffer), format, w, h)

 これでようやく、ストリームを読み込む準備が整いました。

データを読み込む

 これから、(1) パケットを読み、(2) デコードしてフレームにし、(3) フレームが1つできる毎に変換・保存する、という工程によって映像ストリーム全体を処理していきます。

C言語

int frameFinished;
AVPacket packet;

i=0;
while(av_read_frame(pFormatCtx, &packet)>=0) {
  // 映像ストリームからのパケットであることを確認
  if(packet.stream_index==videoStream) {

    // 映像フレームをデコードする
    // [翻訳版注記] avcodec_decode_video() は廃止されています
    avcodec_decode_video(pCodecCtx, pFrame, &frameFinished,
                         packet.data, packet.size);

    // 映像フレームを取得できたか確認
    if(frameFinished) {
    // Convert the image from its native format to RGB

        // [翻訳版注記] img_convert() は廃止されています
        img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24, 
            (AVPicture*)pFrame, pCodecCtx->pix_fmt, 
            pCodecCtx->width, pCodecCtx->height);

        // Save the frame to disk
        if(++i<=5)
          SaveFrame(pFrameRGB, pCodecCtx->width, 
                    pCodecCtx->height, i);
    }
  }

  // av_read_frame() で得られたパケット分のメモリーを開放
  av_free_packet(&packet);
}

swift

// フレームを読み込む
var i = 0
while av_read_frame(formatContext, packet) >= 0  {
    // 対象ストリームからのパケットであることを確認
    if packet.memory.stream_index == Int32(n) {
        // パケットに保存したデータから映像フレームをデコードできたら実行
        if decodeVideo(codecContext, frame: frame, packet: packet) {

            // 画像をRGB形式に変換
            let swsContextOption = sws_getContext(
                codecContext.memory.width,
                codecContext.memory.height,
                codecContext.memory.pix_fmt,
                w, h, 
                PIX_FMT_RGB24, SWS_BILINEAR, 
                nil, nil, nil
            )
            swsScale(swsContextOption, source: frame, target: convertedFrame)

            // 100フレームごとに1つ保存
            if ++i % 100 == 0 {
                saveAsPPM(convertedFrame, filePath: "/Users/tottokotkd/frame\(i).ppm", width: Int(w), height: Int(h))
            }
        }
    }
    // av_read_frame() で得られたパケット分のメモリーを開放
    av_free_packet(packet)
}

[翻訳版注記 開始]
 swift版では、デコードと変換の部分を関数としています。
 デコードに関しては、特に問題はありません。その内容は以下の通りです。

func decodeVideo(codecContext: UnsafeMutablePointer<AVCodecContext>, frame: UnsafeMutablePointer<AVFrame>, packet: UnsafeMutablePointer<AVPacket>) -> Bool {
    var finished = UnsafeMutablePointer<Int32>.alloc(1)
    avcodec_decode_video2(codecContext, frame, finished, packet)
    // 
    return finished.memory.bool
}

extension Int32 {
    var bool: Bool {
        return self != 0 ? true : false
    }
}

 大いに問題があるのは、img_convert() に替えて用意された sws_scale() のラッパー部分です。

func swsScale(option: SwsContext, source: UnsafePointer<AVFrame>, target: UnsafePointer<AVFrame>, height: Int32) -> Int {
    let sourceData = [
        UnsafePointer<UInt8>(source.memory.data.0),
        UnsafePointer<UInt8>(source.memory.data.1),
        UnsafePointer<UInt8>(source.memory.data.2),
        UnsafePointer<UInt8>(source.memory.data.3),
        UnsafePointer<UInt8>(source.memory.data.4),
        UnsafePointer<UInt8>(source.memory.data.5),
        UnsafePointer<UInt8>(source.memory.data.6),
        UnsafePointer<UInt8>(source.memory.data.7),
    ]
    let sourceLineSize = [
        source.memory.linesize.0,
        source.memory.linesize.1,
        source.memory.linesize.2,
        source.memory.linesize.3,
        source.memory.linesize.4,
        source.memory.linesize.5,
        source.memory.linesize.6,
        source.memory.linesize.7
    ]

    let targetData = [
        target.memory.data.0,
        target.memory.data.1,
        target.memory.data.2,
        target.memory.data.3,
        target.memory.data.4,
        target.memory.data.5,
        target.memory.data.6,
        target.memory.data.7
    ]
    let targetLineSize = [
        target.memory.linesize.0,
        target.memory.linesize.1,
        target.memory.linesize.2,
        target.memory.linesize.3,
        target.memory.linesize.4,
        target.memory.linesize.5,
        target.memory.linesize.6,
        target.memory.linesize.7
    ]

    let result = sws_scale(
        option,
        sourceData,
        sourceLineSize,
        0,
        height,
        targetData,
        targetLineSize
    )
    return Int(result)
}

 本来、frame->data のデータ型は uint8_t *[8] となっています。ところがこれをswiftから見ると、8つのデータが入ったタプルになっています。
 sws_scale() は、2, 3, 6, 7番目の引数として「ポインターのポインター」つまりポインターの配列を要求しています。したがって、タプルでは通りません。いろいろ試してみましたがどうにもならず、やむを得ないと考えて人力で配列に戻しています。なにか標準的な方法が用意されていると思いますが、見つけられませんでした。
[翻訳版注記 終了]

[パケットについて]
 技術的には、1つのパケットに不完全なフレームやその他のデータが入っていることがありえます。しかしffmpegのパーサーを通すと、1つのパケットに1つ以上のフレームが含まれることが保証されます。

 既に述べたように、このプロセスは単純です。av_read_frame() によってパケットを読み取り、AVPacket 構造体に保存します。こちらは構造体を確保しただけで、その中身は packet.data 宛にffmpegが入力することに注意してください。これは av_free_packet() によって解放されています。
 avcodec_decode_video() を使うことにより、パケットをフレームに変換します。このとき、パケットをデコードしてもなお1フレーム分の情報を確保できないことがあるため、avcodec_decode_video()frameFinished を通じてフレームを完全に読み取れたかどうか通知します。
 最後に、img_convert() を適用することで元のフォーマット (pCodecCtx->pix_fmt) からRGBに変換します。AVFrame のポインターを AVPicture にキャストできることは記憶しておいてください。

 あとは、フレームそのものとサイズの情報を SaveFrame 関数に渡せば終わりです。ここでは、得られたRGB情報をPPM形式で書き出すだけです。PPM形式そのものについては詳説しませんが、これでちゃんと動きます。

C言語

void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
  FILE *pFile;
  char szFilename[32];
  int  y;

  // ファイルを開く
  sprintf(szFilename, "frame%d.ppm", iFrame);
  pFile=fopen(szFilename, "wb");
  if(pFile==NULL)
    return;

  // ヘッダーを書き込む
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);

  // ピクセルデータを書き込む
  for(y=0; y<height; y++)
    fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);

  // ファイルを閉じる
  fclose(pFile);
}

swift

/**
PPM形式で保存する
*/
func saveAsPPM(frame: UnsafePointer<AVFrame>, filePath: String, width: Int, height: Int) -> Bool {
    // ファイルを開く
    var file = fopen(filePath.cStringUsingEncoding(NSUTF8StringEncoding)!, "wb")
    if file == nil { return false }

    // ヘッダーを書き込む
    let header = "P6\n\(width) \(height)\n255\n"
    fwrite(header.cStringUsingEncoding(NSUTF8StringEncoding)!, 1, UInt(countElements(header)), file)

    // ピクセルデータを書き込む
    for i in 0..<height {
        let y = UnsafeBufferPointer(start: frame.memory.data.0 + i * Int(frame.memory.linesize.0), count: width * 3)
        fwrite(y.baseAddress, 1, UInt(width * 3), file)
    }
    // ファイルを閉じる
    if fclose(file) != 0 {
        return false
    }
    return true
}

 まず普通にファイルを開いてから、RGBデータを書き込んでいます。ファイルは一度に、一行で記録します。PPM形式のファイルは単純に、長い文字列としてRGB情報を持つのです。HTMLの色指定について知っている人なら、真っ赤な画像を表現するために ff0000#ff0000… と書いているようなもの、といえばイメージが湧くでしょうか (実際のPPMは区切りのないバイナリーデータなのでこれとは異なりますが、理解の手がかりにはなるでしょう)。冒頭のヘッダーは画像のサイズとRGB情報の最大値を指定します。

[翻訳版注記 開始]
 swift版には UnsafeBufferPointer が使われています。
 …使っておいてなんですが、どうしてこれで動くのか、翻訳者には全然分かりません。ただそれっぽい型を片っ端から試したら偶然これが動いただけです。
[翻訳版注記 終了]

 さて、main() に戻りましょう。映像ストリームを読み終わったら、後片付けをします。

C言語

//RGB画像のフレームを解放
// [翻訳版注記] av_free() は廃止されています
av_free(buffer);
av_free(pFrameRGB);

// YUV画像のフレームを解放
av_free(pFrame);

// コーデックを閉じる
avcodec_close(pCodecCtx);

// 動画ファイルを閉じる
// [翻訳版注記] av_close_input_file() は廃止されています
av_close_input_file(pFormatCtx);

return 0;

swift

// フレームのメモリーを開放する
av_frame_free(&frame)
av_frame_free(&convertedFrame)

// コーデックを閉じる
avcodec_close(codecContext)

// 動画ファイルを閉じる
avformat_close_input(&formatContext)

 avcode_alloc_frame and av_malloc のどちらで確保したメモリーも、av_free で解放できます。

 コードは以上です! Linuxなどでは、以下のようにビルドしてください。

gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lavutil -lm

 ffmpegのバージョンが低い場合、 -lavutil は外す必要があるかもしれません。

gcc -o tutorial01 tutorial01.c -lavformat -lavcodec -lz -lm

 PPM形式なら、ほとんどの画像処理ソフトで開けます。適当な動画ファイルで試してみましょう。

This work is licensed under the Creative Commons Attribution-Share Alike 2.5 License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/2.5/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.

Code examples are based off of FFplay, Copyright (c) 2003 Fabrice Bellard, and a tutorial by Martin Bohme.

[翻訳版注記 開始]
 上記の権利表示およびライセンス表示は原作品に附されたものです。
 翻訳者は、本訳文の全体をCC BY-SA 2.5のもとで頒布します。サンプルプログラムも同様とします。
[翻訳版注記 終了]

37
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
36