Ffmpegベースのビデオプレーヤー

こんにちは、Habr!



この記事では、FFmpegプロジェクトのライブラリーを使用した単純なプレーヤーの開発に焦点を当てます。

Habréでこのテーマに関する記事を見つけられなかったため、このギャップを埋めることにしました。

ビデオのデコードは、FFmpegライブラリ、ディスプレイ、SDLを使用して実行されます。





はじめに



FFmpegを使用すると、エンコードとデコード、多重化と逆多重化など、多数のビデオ処理タスクを実行できます。 これにより、マルチメディアアプリケーションの開発が大幅に促進されます。



ほとんどのオープンソースプロジェクトと同様に、主な問題の1つはドキュメントです。 それは非常に小さく、常に関連するとは限りません。 これは、絶えず変化するAPIを備えたペースの速いプロジェクトです。 したがって、ドキュメントの主なソースは、ライブラリ自体のソースコードです。 古い記事から、[1]と[2]を読むことをお勧めします。 それらは、一般的なライブラリを扱うという考えを与えます。



FFmpegは、さまざまなメディア形式で作業するためのユーティリティとライブラリのセットです。 ユーティリティについて話すことはおそらく意味がありません-誰もがそれらについて聞いたことがありますが、ライブラリについて詳しく知ることができます。



画面にビデオを表示するには、SDLを使用します。 これは、非常にシンプルなAPIを備えた便利でクロスプラットフォームなフレームワークです。



経験豊富な読者は、そのようなプレーヤーがFFmpegディストリビューションに既に存在し、そのコードがffplay.cファイルで利用可能であり、SDLも使用していることに気付くかもしれません! しかし、そのコードは初心者のFFmpeg開発者にとって理解が非常に難しく、多くの追加機能が含まれています。

同様のプレーヤーも[1]で説明されていますが、FFmpegに含まれていないか、廃止された関数を使用しています。

現在のAPIを使用した、最小限の理解しやすいプレーヤーの例を紹介します。 簡単にするために、音声なしでビデオのみを表示します。

それでは始めましょう。



コード



まず、必要なヘッダーファイルを含めます。

#include <stdio.h> #include <SDL.h> #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h>
      
      





この小さな例では、すべてのコードがメインになります。

まず、 av_register_all()で ffmpegライブラリを初期化します。 初期化中に、ライブラリで使用可能なすべてのファイル形式とコーデックが登録されます。 その後、これらのコーデックを使用して、この形式のファイルを開くときに自動的に使用されます。

 int main(int argc, char* argv[]) { if (argc < 2) { printf("Usage: %s filename\n", argv[0]); return 0; } // Register all available file formats and codecs av_register_all();
      
      





次に、SDLを初期化します。 引数として、 SDL_Init関数は、初期化する必要があるサブシステムのセットを受け取ります(複数のサブシステムを初期化するために論理ORが使用されます)。 この例では、ビデオサブシステムのみが必要です。

 int err; // Init SDL with video support err = SDL_Init(SDL_INIT_VIDEO); if (err < 0) { fprintf(stderr, "Unable to init SDL: %s\n", SDL_GetError()); return -1; }
      
      





次に、入力ファイルを開きます。 ファイル名は、コマンドラインの最初の引数として渡されます。

avformat_open_input関数は、ファイルヘッダーを読み取り、見つかった形式に関する情報をAVFormatContext構造体に格納します。 残りの引数はNULLに設定できます。この場合、libavformatは自動パラメーター検出を使用します。

  // Open video file const char* filename = argv[1]; AVFormatContext* format_context = NULL; err = avformat_open_input(&format_context, filename, NULL, NULL); if (err < 0) { fprintf(stderr, "ffmpeg: Unable to open input file\n"); return -1; }
      
      





なぜなら avformat_open_inputはファイルヘッダーのみを読み取るため 、次のステップはファイル内のストリームに関する情報を取得することです。 これは、 avformat_find_stream_info関数によって行われます。

  // Retrieve stream information err = avformat_find_stream_info(format_context, NULL); if (err < 0) { fprintf(stderr, "ffmpeg: Unable to find stream info\n"); return -1; }
      
      





その後、 format_context-> streamsには既存のすべてのファイルストリームが含まれます。 それらの数はformat_context-> nb_streamsと同じです

av_dump_format関数を使用して、ファイルおよびすべてのストリームに関する詳細情報を表示できます。

 // Dump information about file onto standard error av_dump_format(format_context, 0, argv[1], 0);
      
      





次に、 format_context-> streamsでビデオストリームの番号を取得します 。 この数により、コーデックコンテキスト( AVCodecContext )を取得でき、ファイルの読み取り時にパッケージのタイプを決定するために使用されます。

  // Find the first video stream int video_stream; for (video_stream = 0; video_stream < format_context->nb_streams; ++video_stream) { if (format_context->streams[video_stream]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { break; } } if (video_stream == format_context->nb_streams) { fprintf(stderr, "ffmpeg: Unable to find video stream\n"); return -1; }
      
      





ストリーム内のコーデックに関する情報は、「コーデックコンテキスト」( AVCodecContext )と呼ばれます。 この情報を使用して、必要なコーデック( AVCodec )を見つけて開くことができます。

  AVCodecContext* codec_context = format_context->streams[video_stream]->codec; AVCodec* codec = avcodec_find_decoder(codec_context->codec_id); err = avcodec_open2(codec_context, codec, NULL); if (err < 0) { fprintf(stderr, "ffmpeg: Unable to open codec\n"); return -1; }
      
      





SDLを使用してビデオを出力するためのウィンドウを準備します(ビデオのサイズはわかっています)。 一般に、任意のサイズのウィンドウを作成し、libswscaleを使用してビデオをスケーリングできます。 ただし、簡単にするために、ウィンドウをビデオのサイズにしましょう。

ウィンドウ自体に加えて、ビデオを表示するオーバーレイ(オーバーレイ)も追加する必要があります。 SDLは、画面に画像を描画するための多数のメソッドと、ビデオを表示するために特別に設計されたメソッドをサポートしています。これはYUVオーバーレイと呼ばれます。 YUVはRGBのような色空間です。 Y-輝度成分を表し、UとVは色成分です。 この形式はRGBよりも複雑です。なぜなら、 色情報の一部は破棄され、2 Yサンプルごとに1つのUおよびVサンプルのみが存在できます。 YUVオーバーレイは、YUVデータの配列を取得して表示します。 4種類の形式をサポートしていますが、最も高速なのはYV12です。 別のYUV形式があります-YUV420P。 UとVの配列が逆になっていることを除いて、YV12と同じです。 FFmpegは画像をYUV420Pに変換でき、ほとんどのビデオストリームは既にこの形式に含まれているか、単に変換されます。

したがって、SDLのYV12オーバーレイを使用し、FFmpegのビデオをYUV420P形式に変換し、表示時にUおよびV配列の順序を変更します。

  SDL_Surface* screen = SDL_SetVideoMode(codec_context->width, codec_context->height, 0, 0); if (screen == NULL) { fprintf(stderr, "Couldn't set video mode\n"); return -1; } SDL_Overlay* bmp = SDL_CreateYUVOverlay(codec_context->width, codec_context->height, SDL_YV12_OVERLAY, screen);
      
      





ピクセル形式の変換とFFmpegのスケーリングは、libswscaleを使用して行われます。

変換は2段階で実行されます。 最初のステップは、変換コンテキスト( struct SwsContext )を作成することです。 以前は、 sws_getContextというフレンドリ名の関数がこれ使用されていました。 ただし、現在は非推奨であり、コンテキストの作成はsws_getCachedContextを使用して行うことをお勧めします。 それを使用します。

  struct SwsContext* img_convert_context; img_convert_context = sws_getCachedContext(NULL, codec_context->width, codec_context->height, codec_context->pix_fmt, codec_context->width, codec_context->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); if (img_convert_context == NULL) { fprintf(stderr, "Cannot initialize the conversion context\n"); return -1; }
      
      





さて、ここで最も興味深い部分、つまりビデオディスプレイについて説明します。

ファイルからのデータはバッチ( AVPacket )で読み取られ、フレーム( AVFrame )が表示に使用されます。

ビデオストリームに関連するパッケージにのみ関心があります(ビデオストリームの番号をvideo_stream変数に保存したことを思い出してください )。

avcodec_decode_video2関数は、以前に受信したコーデック( codec_context )を使用してパケットをフレームにデコードします。 この関数は、フレーム全体がデコードされる場合、 frame_finishedに正の値を設定します(つまり、1つのフレームが複数のパケットを占有でき、 frame_finishedは最後のパケットのデコード時にのみ設定されます)。

  AVFrame* frame = avcodec_alloc_frame(); AVPacket packet; while (av_read_frame(format_context, &packet) >= 0) { if (packet.stream_index == video_stream) { // Video stream packet int frame_finished; avcodec_decode_video2(codec_context, frame, &frame_finished, &packet); if (frame_finished) {
      
      





次に、ウィンドウに表示する画像を準備する必要があります。 まず、データを書き込むため、オーバーレイをブロックします。 ファイル内のビデオは任意の形式にすることができ、YV12用にディスプレイを構成しました。 Libswscaleが助けになります。 前に、 img_convert_context変換コンテキストをセットアップしました。 それを適用する時間です。 主なlibswscaleメソッドは、もちろんsws_scaleです。 彼は必要な変換を実行します。 配列を割り当てるときは、インデックスの不整合に注意してください。 これはタイプミスではありません。 前述のように、YUV420Pは、色成分の順序が異なるという点でのみYV12と異なります。 libswscaleをYUV420Pに変換するように設定し、SDLはYV12を期待しています。 ここでは、すべてが正しくなるようにUとVの置換を行います。

  SDL_LockYUVOverlay(bmp); AVPicture pict; pict.data[0] = bmp->pixels[0]; pict.data[1] = bmp->pixels[2]; // it's because YV12 pict.data[2] = bmp->pixels[1]; pict.linesize[0] = bmp->pitches[0]; pict.linesize[1] = bmp->pitches[2]; pict.linesize[2] = bmp->pitches[1]; sws_scale(img_convert_context, frame->data, frame->linesize, 0, codec_context->height, pict.data, pict.linesize); SDL_UnlockYUVOverlay(bmp);
      
      





オーバーレイの画像をウィンドウに表示します。

 SDL_Rect rect; rect.x = 0; rect.y = 0; rect.w = codec_context->width; rect.h = codec_context->height; SDL_DisplayYUVOverlay(bmp, &rect);
      
      





パッケージを処理した後、パッケージが占有しているメモリを解放する必要があります。 これは、 av_free_packet関数によって行われます。

  } } // Free the packet that was allocated by av_read_frame av_free_packet(&packet);
      
      





OSがアプリケーションのハングを考慮せず、ウィンドウを閉じるときにアプリケーションを完了するために、サイクルの最後に1つのSDLイベントを処理します。

 // Handling SDL events there SDL_Event event; if (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { break; } } }
      
      





さて、今ではすべての使用済みリソースをクリーニングするための標準手順です。

 sws_freeContext(img_convert_context); // Free the YUV frame av_free(frame); // Close the codec avcodec_close(codec_context); // Close the video file avformat_close_input(&format_context); // Quit SDL SDL_Quit(); return 0; }
      
      







議会に渡します。 gccを使用する最も簡単なオプションは次のようになります。

 gcc player.c -o player -lavutil -lavformat -lavcodec -lswscale -lz -lbz2 `sdl-config --cflags --libs`
      
      





始めます。 そして、私たちは何を見ますか? ビデオは途方もない速度で再生されています! 正確には、ファイルからのフレームの読み取りとデコードの速度で再生が行われます。 本当に。 フレームレートを制御するコードを1行も記述しませんでした。 そして、このトピックはすでに別の記事のためのものです。 このコードで改善できる多くのことがあります。 たとえば、サウンドの再生を追加したり、他のストリームでファイルを読み込んで表示したりします。 Habrasocietyに興味がある場合は、次の記事でこれについて説明します。



ソースコード全体。



ご清聴ありがとうございました!



続き: ffmpegビデオプレーヤーの完成



参照資料



  1. ffmpegとSDLチュートリアル (en)
  2. libavformatおよびlibavcodecの使用 (en)



All Articles