FFmpegでトランスコード

Qiita初投稿です。よろしくお願いします。
以前ffmpegを使ってトランスコード処理を実装したことがあったので、せっかくなのでまとめておこうと思いました。

# 参考
* http://proc-cpuinfo.fixstars.com/2017/08/ffmpeg-api-decode/
* https://ffmpeg.org/doxygen/trunk/doc_2examples_2transcoding_8c-example.html

# ソースコード
ここにあります。
> https://github.com/sana37/FFmpegSample/blob/master/src/transcode.cpp

※注意：以前別環境で動いていたものをまとめる用に書き直しただけなのでそのまま使っても動きません。参考程度にしてください

# 解説

## 全体の流れ

大まかに、

* 元の動画からパケットを取得
* パケットからビデオフレームを取得（デコード処理）
* フレームをRGBAに変換
* RGBAフレームになんかする（例えばシェーダ使って）
* RGBAから別のピクセルフォーマットに変換（YUV420pとか。エンコーダが対応しているものを選択）
* フレームをためてパケットを生成（エンコード処理）
* パケットを集めて新しい動画ファイルを作る

といった感じです。

ちなみに、パケットが音声パケットだった場合はトランスコードを行わず、そのまま出力先動画ファイルにわたしています。

## デコーダのセットアップ
setupDecoding関数で行っています。

動画ファイルはビデオストリームと音声ストリームで構成されています。
今回トランスコードするのはビデオストリームだけなので、ビデオ用のコーデックのみ取得します。

```cpp
  // Find the first video stream
  videoStrmDec = NULL;
  int i;
  for (i = 0; i < fmtCtxDec->nb_streams; i++) {
    if (fmtCtxDec->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
      videoStrmDec = fmtCtxDec->streams[i];
      break;
    }
  }
```

複数あるストリームの中からビデオストリームを取得して、

```cpp
  // Find the decoder for the video stream
  AVCodec *codecDec = avcodec_find_decoder(videoStrmDec->codecpar->codec_id);
```

ビデオストリームの情報を元にコーデックを取得します。

また、フレームのピクセルフォーマット変換用の変数を次のように設定しています。

```cpp
  //get the scaling context
  swsCtxDec = sws_getContext
    (
        codecCtxDec->width,
        codecCtxDec->height,
        codecCtxDec->pix_fmt,
        codecCtxDec->width,
        codecCtxDec->height,
        AV_PIX_FMT_RGBA,
        SWS_BICUBIC,
        NULL,
        NULL,
        NULL
    );
```

今回は解像度の変更はしないので、変換前と変換後でwidthとheightが同じになるようにしています。
変換後のピクセルフォーマットは`AV_PIX_FMT_RGBA`を設定することでRGBAにします。

## エンコーダのセットアップ
setupEncoding関数で行っています。

動画のエンコード方式にはH264を指定します。

```cpp
  AVCodec *codecEnc = avcodec_find_encoder(AV_CODEC_ID_H264);
```

H264のコーデックを取得

```cpp
  if (codecEnc->pix_fmts)
    codecCtxEnc->pix_fmt = codecEnc->pix_fmts[0];
  else
    codecCtxEnc->pix_fmt = codecCtxDec->pix_fmt;
```

そしてここで、H264コーデックが対応しているピクセルフォーマットを取得しています。
（今思ったんですけど、これ、else文に移ったときちゃんとエンコードできるのかわからない）

あと、ここ

```cpp
  // set picture properties
  codecCtxEnc->width = codecCtxDec->width;
  codecCtxEnc->height = codecCtxDec->height;
  codecCtxEnc->field_order = AV_FIELD_PROGRESSIVE;
  codecCtxEnc->color_range = codecCtxDec->color_range;
  codecCtxEnc->color_primaries = codecCtxDec->color_primaries;
  codecCtxEnc->color_trc = codecCtxDec->color_trc;
  codecCtxEnc->colorspace = codecCtxDec->colorspace;
  codecCtxEnc->chroma_sample_location = codecCtxDec->chroma_sample_location;
  codecCtxEnc->sample_aspect_ratio = codecCtxDec->sample_aspect_ratio;
  codecCtxEnc->profile = FF_PROFILE_H264_BASELINE; // <- ここ
```

と、ここ

```cpp
  // make codec options
  AVDictionary *dictEnc = NULL;
  av_dict_set(&dictEnc, "preset", "medium", 0);
  av_dict_set(&dictEnc, "crf", "22", 0);
  av_dict_set(&dictEnc, "profile", "baseline", 0); // <- ここ
  av_dict_set(&dictEnc, "level", "4.0", 0);
```

で、プロファイルをbaselineに指定しているんですけど、たしかどっちかが必須でどっちかは意味なかったような気がします。
まあ、とりあえず両方とも書いておけば間違いないです（オイ

続いて、元ファイルからストリーム情報をコピーし、新しいストリームを生成します。

```cpp
  int i;
  for (i = 0; i < fmtCtxDec->nb_streams; i++) {
    AVStream *strmDec = fmtCtxDec->streams[i];

    if (i == videoStrmDec->index) {
      // create video stream
      videoStrmEnc = avformat_new_stream(fmtCtxEnc, codecEnc);
      if (videoStrmEnc == NULL) {
        fprintf(stderr, "avformat_new_stream failed");
        return -1;
      }

      videoStrmEnc->sample_aspect_ratio = codecCtxEnc->sample_aspect_ratio;
      videoStrmEnc->time_base = codecCtxEnc->time_base;

      if (avcodec_parameters_from_context(videoStrmEnc->codecpar, codecCtxEnc) < 0) {
        fprintf(stderr, "avcodec_parameters_from_context failed");
        return -1;
      }

      AVDictionary *videoDictDec = videoStrmDec->metadata;
      AVDictionary *videoDictEnc = NULL;
      av_dict_copy(&videoDictEnc, videoDictDec, 0);
      videoStrmEnc->metadata = videoDictEnc;

    } else if (strmDec->codecpar->codec_type == AVMEDIA_TYPE_UNKNOWN) {
      fprintf(stderr, "Elementary stream %d is of unknown type, cannot proceed.", i);
      return -1;
    } else {
      // create audio stream
      AVStream *strmEnc = avformat_new_stream(fmtCtxEnc, NULL);
      if (strmEnc == NULL) {
        fprintf(stderr, "avformat_new_stream failed");
        return -1;
      }
      if (avcodec_parameters_copy(strmEnc->codecpar, strmDec->codecpar) < 0) {
        fprintf(stderr, "Copying parameters for stream %d failed.", i);
        return -1;
      }
      strmEnc->time_base = strmDec->time_base;
    }
  }
```

音声ストリームとビデオストリームでコピーの仕方がそれぞれ違っています。

```cpp
  swsCtxEnc = sws_getContext
    (
        codecCtxEnc->width,
        codecCtxEnc->height,
        AV_PIX_FMT_RGBA,
        codecCtxEnc->width,
        codecCtxEnc->height,
        codecCtxEnc->pix_fmt,
        SWS_BICUBIC,
        NULL,
        NULL,
        NULL
    );
```

そして、デコーダのセットアップ時と同様に、ピクセルフォーマット変換用の変数を設定します。
デコード時にRGBAに変換したものを今度はエンコード用のピクセルフォーマットに変換します。

## AVFrameの初期化

`setupAVFrames`関数では、AVFrameのメモリ確保をしたりピクセルフォーマットを設定したりします。

`frameDec`変数はデコーダから取得したフレームを格納する用です。

`frameRGBA`変数には`frameDec`のフレームをRGBA形式に変換したものを格納します。

`frameEnc`変数は`frameRGBA`のRGBAフレームをエンコード用のピクセルフォーマットに変換したものを格納します。

`frameDec`だけ`av_frame_get_buffer`や各種設定をしていませんが、これはおそらくデコーダが勝手に設定してくれるから大丈夫です。

## メインの処理

```cpp
  int frameIndex = 0;
  AVPacket pktDec;
  while (av_read_frame(fmtCtxDec, &pktDec) >= 0) {
    int strmIdx = pktDec.stream_index;
    // Is this a packet from the video stream?
    if (strmIdx == videoStrmDec->index) {
      // Decode video frame
      if (avcodec_send_packet(codecCtxDec, &pktDec) != 0) {
        fprintf(stderr, "avcodec_send_packet failed\n");
        return -1;
      }
      while (avcodec_receive_frame(codecCtxDec, frameDec) == 0) {
        if (onFrameDecoded(frameIndex) != 0)
          return -1;
        frameIndex++;
      }
    } else {
      // Remux this frame without reencoding
      av_packet_rescale_ts(&pktDec, fmtCtxDec->streams[strmIdx]->time_base, fmtCtxEnc->streams[strmIdx]->time_base);
      if (av_interleaved_write_frame(fmtCtxEnc, &pktDec) < 0) {
        fprintf(stderr, "av_interleaved_write_frame without reencoding failed...");
        return -1;
      }
    }
    // Free the packet that was allocated by av_read_frame
    av_packet_unref(&pktDec);
  }
```

`av_read_frame`関数で元ファイルからパケットを取得していきます。
パケットが音声ストリームのものであったならそのまま出力先ファイルにわたし、
ビデオストリームのものであったならデコードします。

`avcodec_send_packet`でデコーダにパケットをわたして、
`avcodec_receive_frame`でデコーダからフレームを取得します。
デコーダはパケットからフレームを作る作業をやってくれてるわけです。

フレームが取得できたら`onFrameDecoded`関数でその先の処理をしています。

```cpp
int onFrameDecoded(int frameIndex) {
  printf("sws_scale to RGBA %d", frameIndex);
  // Convert the image from its native format to RGBA
  sws_scale
  (
    swsCtxDec,
    (uint8_t const * const *) frameDec->data,
    frameDec->linesize,
    0,
    frameDec->height,
    frameRGBA->data,
    frameRGBA->linesize
  );

  printf("filtering frame %d", frameIndex);
  filterFrame(frameRGBA);

  printf("sws_scale from RGBA %d", frameIndex);
  // Convert the image from RGBA to YUV420p?.
  sws_scale
  (
    swsCtxEnc,
    (uint8_t const * const *) frameRGBA->data,
    frameRGBA->linesize,
    0,
    frameRGBA->height,
    frameEnc->data,
    frameEnc->linesize
  );
  /**
   * Encode
   */
  printf("encode the frame %d", frameIndex);
  int64_t pts = av_frame_get_best_effort_timestamp(frameDec);
  frameEnc->pts = av_rescale_q(pts, videoStrmDec->time_base, codecCtxEnc->time_base);
  frameEnc->key_frame = 0;
  frameEnc->pict_type = AV_PICTURE_TYPE_NONE;

  if (encodeFrame(frameEnc) != 0) {
    fprintf(stderr, "encodeFrame failed...");
    return -1;
  }
  printf("encoding frame %d has done.", frameIndex);

  return 0;
}
```

`sws_scale`関数がピクセルフォーマットの変換をしてくれます。
第２引数に変換元のデータをわたし、第６引数には変換後データの格納先をわたしています。
`AVFrame`のメンバ変数`data`はフレームの実際のデータ配列がそのまんま入っているので、これを渡せばokです。

RGBAフレームに対しては何か好きな処理を行ってください（`filterFrame`関数）

２回ぶんのピクセルフォーマット変換が終わったら、エンコード処理に入ります。
`encodeFrame`関数でエンコード処理を行っています。

```cpp
int encodeFrame(AVFrame *frame) {
  if (avcodec_send_frame(codecCtxEnc, frame) != 0) {
    fprintf(stderr, "avcodec_send_frame failed");
    return -1;
  }

  AVPacket pktEnc;
  av_init_packet(&pktEnc);
  pktEnc.data = NULL;
  pktEnc.size = 0;
  while (1) {
    int result = avcodec_receive_packet(codecCtxEnc, &pktEnc);
    if (result == AVERROR(EAGAIN)) {
      printf("more frames are needed");
      break;
    } else if (result == AVERROR_EOF) {
      printf("the encoder has been fully flushed.");
      break;
    } else if (result < 0) {
      fprintf(stderr, "error during encoding.  receive_packet failed.");
      return -1;
    }
    pktEnc.stream_index = videoStrmEnc->index;
    av_packet_rescale_ts(&pktEnc, codecCtxEnc->time_base, videoStrmEnc->time_base);

    if (av_interleaved_write_frame(fmtCtxEnc, &pktEnc) != 0) {
      fprintf(stderr, "av_interleaved_write_frame failed\n");
      return -1;
    }
  }

  return 0;
}
```

エンコーダはフレームを受け取ってパケットを生成します。
`avcodec_send_frame`でエンコーダにフレームをわたして、
`avcodec_receive_packet`でエンコーダからパケットを取得します。

パケットを生成するには複数のフレームが必要なので、1回フレームをわたしてもまだパケットができなかった場合は`AVERROR(EAGAIN)`が返ってきます。
その場合はbreakしてこの関数から抜けて、次のフレームのデコードに移ります。

`avcodec_receive_packet`がゼロを返したときはパケット取得成功なので、パケットに対しストリーム情報やタイムスタンプ情報などを付加してから出力ファイルへの書き込みをします。
`av_interleaved_write_frame`関数がパケットの書き込みをしてくれます。

これらの処理を繰り返し、トランスコードしていきます。

## デコーダ・エンコーダのフラッシュとか

```cpp
  // flush decoder
  printf("flush decoder");
  if (avcodec_send_packet(codecCtxDec, NULL) != 0) {
    fprintf(stderr, "flush avcodec_send_packet failed\n");
    return -1;
  }
  while (avcodec_receive_frame(codecCtxDec, frameDec) == 0) {
    if (onFrameDecoded(frameIndex) != 0)
      return -1;
    frameIndex++;
  }

  // flush encoder
  printf("flush encoder");
  if (encodeFrame(NULL) != 0) {
    fprintf(stderr, "flush encoder failed...");
    return -1;
  }

  printf("write trailer");
  if (av_write_trailer(fmtCtxEnc) != 0) {
    fprintf(stderr, "av_write_trailer failed\n");
    return -1;
  }
```

メイン処理での`av_read_frame`がパケットを返してこなくなったとしても、デコーダとエンコーダにはまだ処理中のデータが残っています。
それらを全部出して最後のデータまで書きこまなくちゃいけないので、これらフラッシュ処理を行います。

`avcodec_send_packet`の第2引数にNULLを渡すと、もうパケットがないことをデコーダに教えることができます。
そうすると、デコーダは`avcodec_receive_frame`で残りのフレームを全部返してくれます（返すフレームは１つずつなのでwhile文で繰り返し取得します）。

デコーダのフラッシュが終わったらエンコーダのフラッシュを行います。
`encodeFrame`は私が作った関数ですが、こいつにNULLを渡すと、内部で`avcodec_send_frame`にNULLをわたしてくれます。

そうすると、もうフレームがないことをエンコーダに教えることができ、以降、エンコーダは`avcodec_receive_packet`で残りのパケットを返してくれるようになります。
一番最後のパケットを返し終わると、`avcodec_receive_packet`は戻り値として`AVERROR_EOF`を返してくれるので、それを得られしだいbreakして`encodeFrame`を抜けます。

最後はファイルに対しトレイラを書き込み、動画ファイル生成完了です。
その後、コーデックやその他の変数のリリースを行って終了です。

# 最後に

ffmpegを使ったトランスコードについて簡単に説明してきましたが、私自身、専門用語のなど正しい使い方ができているかわからない部分があります。
間違った理解を与えてしまったらすみません。

また、このプログラムはqiitaに投稿するように簡単にまとめなおしたものなので、なおしてからはプログラムの動作確認をしていません。
参考程度にするのをお勧めします。