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関数で行っています。
動画ファイルはビデオストリームと音声ストリームで構成されています。
今回トランスコードするのはビデオストリームだけなので、ビデオ用のコーデックのみ取得します。
// 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に投稿するように簡単にまとめなおしたものなので、なおしてからはプログラムの動作確認をしていません。
参考程度にするのをお勧めします。