LoginSignup
8
10

More than 5 years have passed since last update.

FFmpegでトランスコード

Posted at

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

参考

ソースコード

ここにあります。

https://github.com/sana37/FFmpegSample/blob/master/src/transcode.cpp

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

解説

全体の流れ

大まかに、

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

といった感じです。

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

デコーダのセットアップ

setupDecoding関数で行っています。

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

  // 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;
    }
  }

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

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

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

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

  //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を指定します。

  AVCodec *codecEnc = avcodec_find_encoder(AV_CODEC_ID_H264);

H264のコーデックを取得

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

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

あと、ここ

  // 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; // <- ここ

と、ここ

  // 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に指定しているんですけど、たしかどっちかが必須でどっちかは意味なかったような気がします。
まあ、とりあえず両方とも書いておけば間違いないです(オイ

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

  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;
    }
  }

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

  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や各種設定をしていませんが、これはおそらくデコーダが勝手に設定してくれるから大丈夫です。

メインの処理

  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関数でその先の処理をしています。

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

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

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

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関数がパケットの書き込みをしてくれます。

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

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

  // 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で残りのフレームを全部返してくれます(返すフレームは1つずつなのでwhile文で繰り返し取得します)。

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

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

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

最後に

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

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

8
10
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
8
10