前回の記事では、ffmpegの主要なコンポーネントが検討され、それらに基づいて、同期せずにデコード速度でビデオを再生する単純なプレーヤーが構築されました。
この記事では、サウンド再生を追加し、同期を処理する方法について説明します。
はじめに
マルチメディアを操作するためのほとんどのアプリケーションとマルチメディアフレームワークは、グラフに基づいています。 グラフノードは、特定のタスクを実行するオブジェクトです。 たとえば、このようなビデオプレーヤーのグラフを考えてみましょう。
作業は
データ読み取りブロックから始まります。このブロックでは、ファイルが読み取られます。 より一般的なケースでは、ネットワーク経由またはハードウェアソースからデータを受信しています。
逆多重化は、着信ストリームを複数の発信ストリーム(オーディオやビデオなど)に
分割します 。 逆多重化は、データコンテナのレベルで機能します。つまり、この段階では、このストリームまたはそのストリームがどのコーデックにエンコードされているかはまったく関係ありません。 コンテナの例:AVI、MPEG-TS、MP4、FLV。
逆多重化後、ブロック
Video Decodingおよび
Audio Decodingで受信したストリームの
デコードが実行されます。 デコーダーは、ビデオの場合はYUVまたはRGBフレーム、オーディオの場合はPCMデータの標準形式でデータを出力します。 デコードは通常、別々のストリームで行われます。
ビデオを表示すると、ビデオが画面に表示され、
オーディオを再生すると、結果のオーディオストリームが再生されます。
実装
プレーヤーにも同様の実装を使用します。 別のストリームで、ファイルを読み取り、ストリームに逆多重化し、他の2つのストリームのビデオとオーディオをデコードします。 次に、SDLを使用して画面に画像を表示し、オーディオを再生します。 アプリケーションのメインスレッドで、SDLイベントを処理します。
この例のコードは非常に大きいことが判明したため、ここでは完全に説明しないことにしました。 基本的なポイントのみを示します。 すべてのコードは、記事の最後にあるリンクで表示できます。
まず、すべての主要な変数を1つの共通のコンテキストに結合します。
typedef struct MainContext { AVFormatContext *format_context;
このコンテキストはすべてのスレッドに渡されます。
video_streamと
audio_streamには、それぞれ情報とビデオとオーディオのストリームが含まれています。
videoqと
audioqは、逆多重化ストリームが読み取りパケットを追加するキューです。 これらの変更を考慮すると、demuxコード(
demux_thread )は完全にシンプルになり、次の形式を取ります。
AVPacket packet; while (av_read_frame(main_context->format_context, &packet) >= 0) { if (packet.stream_index == video_stream_index) {
ここで、ファイルから次のパケットを読み取り、ストリームのタイプに応じて、デコードのために適切なキューに入れます。 これは逆多重化ストリームの主な機能であり、それ以上のアクションは実行されません。
ここで、より複雑なビデオおよびオーディオデコードストリームを検討します。
ビデオを再生する
ビデオのデコードと表示のプロセスについては、以前の記事で説明しました。 メインコードは変更されませんでしたが、2つの部分に分割されました。ビデオデコードは、個別のストリーム(
video_decode_thread )で実行され、ウィンドウ表示(
video_refresh_timer )は、タイマーによってメインストリームで実行されます。 この分離は、同期の実装を容易にするために必要です。これについては、記事の後半で検討します。
SDL
ではビデオの操作がアプリケーションのメインスレッドで実行される
必要があるため、イメージの更新は個別のスレッドではなくタイマーによって行われます。 同様に、任意のストリームからオーバーレイを作成することはできません。 この制限は、たとえばSDLイベントと条件変数を使用して回避できます。 しかし、我々はそれをしません。 デコードを開始する前に作成する1つのオーバーレイに制限します。
オーディオを再生する
コンピューターの音は
サンプルの連続した流れです。 各サンプルは波形値です。 サウンドは特定の
サンプリングレートで記録され、同じ周波数で再生する必要があります。
サンプリングレートは、1秒あたりのサンプル数です。 たとえば、1秒あたり44,100サンプルは、オーディオCDのサンプリング周波数です。 さらに、オーディオには複数のチャンネルを含めることができます。 たとえば、ステレオサンプルの場合、一度に2つになります。 ファイルからデータを受信する場合、受信されるサンプルの数はわかりませんが、FFmpegは不完全なサンプルを提供しません。 これは、FFmpegがステレオサンプルを分割しないことも意味します。
最初のステップは、音声出力用にSDLを構成することです。 初期化関数にフラグ
SDL_INIT_AUDIOを追加する必要があります。 次に、
SDL_AudioSpec構造体に入力し、
SDL_OpenAudio関数に
渡します。
SDL_AudioSpec wanted_spec, spec;
SDLはコールバック関数呼び出しを使用して音声を出力します。
構造には次のパラメーターがあります。
- freq :サンプリングレート。
- format :送信データの形式。 「AUDIO_S16SYS」の「S」記号は、データが署名されることを意味します。16-サンプルサイズは16ビット、「SYS」-システムバイトオーダーが使用されます。 FFmpegがデコードされたデータを返すのはこの形式です。
- channels :オーディオチャネルの数。
- 沈黙 : 沈黙の意味。 署名されたデータの場合、通常0が使用されます。
- samples :SDLオーディオバッファのサイズ。 通常の値は512〜8192バイトです。 1024を使用します。
- callback :バッファにデータを入力するコールバック関数。
- userdata :コールバック関数に渡されるユーザーデータ。 ここではメインコンテキストを使用します。
SDL_PauseAudio(0)を呼び出すと、サウンドの再生が開始されます。 バッファにデータがない場合、「無音」が再生されます。
オーディオデコード
おそらく覚えているように、逆多重化するとき、読み取りパケットを別の
audioqキューに入れます 。
audio_decode_threadデコード
関数の主な目的は、キューからパケットを取得し、デコードして別のバッファーに入れること
です 。これは、
SDL_OpenAudioで指定した関数で読み取られます。
このような
バッファとして
循環バッファを使用します。 主な機能のプロトタイプ:
int ring_buffer_write(RingBuffer* rb, void* buffer, int len, int block); int ring_buffer_read(RingBuffer* rb, void* buffer, int len, int block);
引数の目的は、名前から明確にする必要があります。
block引数は、十分なバッファスペースがない場合、または読み取るデータがない場合に関数を
ブロックするかどうかを示します。
したがって、デコード機能全体は次のとおりです。
static int audio_decode_thread(void *arg) { assert(arg != NULL); MainContext* main_context = (MainContext*)arg; Stream* audio_stream = &main_context->audio_stream; AVFrame frame; while (1) { avcodec_get_frame_defaults(&frame);
オーディオパケットは
avcodec_decode_audio4関数によって
デコードされますフレーム全体が
デコードされた場合(
got_frameフラグ)、
av_samples_get_buffer_size関数を使用してバイト単位でバッファーのサイズを決定し、リングバッファーに書き込みます。
オーディオを再生する
つまり、デコードされたサンプルを再生するために少しだけ残っています。 これは、コールバック関数
audio_callbackで行われます。
static void audio_callback(void* userdata, uint8_t* stream, int len) { assert(userdata != NULL); MainContext* main_context = (MainContext*)userdata; ring_buffer_read(&main_context->audio_buf, stream, len, 1); }
ここではすべてが基本です。
lenバッファーからバイトを取得し、提供されたSDLバッファーに保存します。
ビデオとは異なり、音声はすぐに正しい速度で再生されます。 これは、オーディオ出力の設定時にサンプリングレートが明示的に指定され、その周波数でSDLコールバック関数が呼び出されるためです。
同期する
ファイル内のビデオおよびオーディオストリームには、再生が必要な瞬間と速度に関する情報が含まれています。 オーディオストリームの場合、これは前のパートで満たしたサンプリングレートであり、ビデオストリームの場合、これは1秒あたりのフレーム数(
FPS )です。 ただし、コンピューターは理想的なデバイスではなく、ほとんどのビデオファイルにはこれらのパラメーターの値が不正確であるため、これらの値に基づいてのみ同期することはできません。 代わりに、ストリーム内の各パケットには、デコードタイムスタンプ(DTS)とプレゼンテーションタイムスタンプ(
PTS )の2つの値が含まれています。 2つの異なる値が存在するのは、ファイル内のフレームが乱れる可能性があるためです。 これは、ビデオに
Bフレームがある場合に可能です(
双予測画像 、前のフレームと次のフレームの両方に依存するフレーム)。 ビデオ上にフレームが繰り返される場合もあります。
3つの同期オプションがあります。
- ビデオのオーディオへの同期。
- オーディオからビデオへの同期。
- ビデオとオーディオの外部ジェネレーターとの同期。
これらのオプションのうち最も単純なもの、つまり
ビデオからオーディオへの
同期を検討してください。 現在のフレームを表示した後、PTSに基づいて次のフレームの表示時間を計算します。 SDLタイマーを使用してイメージを更新します。
メインコンテキストで、次のフィールドを追加します。
typedef struct MainContext { double video_clock; double audio_clock; double frame_timer; double frame_last_pts; double frame_last_delay; } MainContext;
- video_clock :ビデオ表示周波数。
- audio_clock :オーディオ再生周波数。
- frame_timer :現在の表示時間値。
- frame_last_pts :表示される最後のフレームのPTS値。
- frame_last_delay :表示される最後のフレームの遅延値。
初期化中に、初期値を
frame_timerに割り当てます。
main_context->frame_timer = (double)av_gettime() / 1000000.0;
ビデオデコードストリームでは、次のフレームの表示時間を計算します。
double pts = frame.pkt_dts; if (pts == AV_NOPTS_VALUE) { pts = frame.pkt_pts; } if (pts == AV_NOPTS_VALUE) { pts = 0; } pts *= av_q2d(main_context->video_stream->time_base); pts = synchronize_video(main_context, &frame, pts);
pts値は、次の3つの値のいずれかを取ることができます。
- frame.pkt_dts :FFmpegは、DTS値がデコードされたフレームのPTS値と一致するように、デコード中にフレームを並べ替えます。 この場合、DTSを使用します。
- frame.pkt_pts :DTS値がない場合、PTSを使用してみてください。
- 0 :両方の値が欠落している場合、最後に保存されたビデオ周波数値を使用します。
機能コード
synchronize_video :
double synchronize_video(MainContext* main_context, AVFrame *src_frame, double pts) { assert(main_context != NULL); assert(src_frame != NULL); AVCodecContext* video_codec_context = main_context->video_stream->codec; if(pts != 0) { main_context->video_clock = pts; } else { pts = main_context->video_clock; } double frame_delay = av_q2d(video_codec_context->time_base); frame_delay += src_frame->repeat_pict * (frame_delay * 0.5); main_context->video_clock += frame_delay; return pts; }
その中で、ビデオの頻度を更新し、可能な繰り返しフレームも考慮します。
オーディオデコードストリームでは、後で周波数が同期されるように、オーディオ周波数を保存します。
if (pkt.pts != AV_NOPTS_VALUE) { main_context->audio_clock = av_q2d(main_context->audio_stream->time_base) * pkt.pts; } else { main_context->audio_clock += (double)data_size / (audio_codec_context->channels * audio_codec_context->sample_rate * av_get_bytes_per_sample(audio_codec_context->sample_fmt)); }
ビデオ表示機能では、次のフレームを表示する前に遅延を計算します。
double delay = compute_delay(main_context); schedule_refresh(main_context, (int)(delay * 1000 + 0.5));
さて、同期の「中心」は
compute_delay関数です。
static double compute_delay(MainContext* main_context) { double delay = main_context->pict.pts - main_context->frame_last_pts; if (delay <= 0.0 || delay >= 1.0) {
最初に、前のフレームと現在のフレームの間の遅延を計算し、現在の値を保存します。 その後、音声との非同期の可能性を考慮し、次のフレームまでに必要な遅延時間を計算します。
以上です! プレーヤーを起動し、視聴をお楽しみください!
おわりに
このパートでは、最も単純なプレーヤーの開発を完了し、プログラムの構造を改善し、オーディオの再生と同期を追加しました。
レビューされていないトピックのうち、巻き戻し、高速/低速再生、その他の同期オプションが残っています。
コーディングと多重化の完全に別のトピックがまだあります。 おそらく、次の記事でそれを検討しようと思います。
プレーヤーのソースコード 。
ご清聴ありがとうございました!