この記事では、サウンド再生を追加し、同期を処理する方法について説明します。
はじめに
マルチメディアを操作するためのほとんどのアプリケーションとマルチメディアフレームワークは、グラフに基づいています。 グラフノードは、特定のタスクを実行するオブジェクトです。 たとえば、このようなビデオプレーヤーのグラフを考えてみましょう。
作業はデータ読み取りブロックから始まります。このブロックでは、ファイルが読み取られます。 より一般的なケースでは、ネットワーク経由またはハードウェアソースからデータを受信しています。
逆多重化は、着信ストリームを複数の発信ストリーム(オーディオやビデオなど)に分割します 。 逆多重化は、データコンテナのレベルで機能します。つまり、この段階では、このストリームまたはそのストリームがどのコーデックにエンコードされているかはまったく関係ありません。 コンテナの例:AVI、MPEG-TS、MP4、FLV。
逆多重化後、ブロックVideo DecodingおよびAudio Decodingで受信したストリームのデコードが実行されます。 デコーダーは、ビデオの場合はYUVまたはRGBフレーム、オーディオの場合はPCMデータの標準形式でデータを出力します。 デコードは通常、別々のストリームで行われます。 ビデオを表示すると、ビデオが画面に表示され、 オーディオを再生すると、結果のオーディオストリームが再生されます。
実装
プレーヤーにも同様の実装を使用します。 別のストリームで、ファイルを読み取り、ストリームに逆多重化し、他の2つのストリームのビデオとオーディオをデコードします。 次に、SDLを使用して画面に画像を表示し、オーディオを再生します。 アプリケーションのメインスレッドで、SDLイベントを処理します。
この例のコードは非常に大きいことが判明したため、ここでは完全に説明しないことにしました。 基本的なポイントのみを示します。 すべてのコードは、記事の最後にあるリンクで表示できます。
まず、すべての主要な変数を1つの共通のコンテキストに結合します。
typedef struct MainContext { AVFormatContext *format_context; // Streams Stream video_stream; Stream audio_stream; // Queues PacketQueue videoq; PacketQueue audioq; /* ... */ } MainContext;
このコンテキストはすべてのスレッドに渡されます。 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) { // Video packet packet_queue_put(&main_context->videoq, &packet); } else if (packet.stream_index == audio_stream_index) { // Audio packet packet_queue_put(&main_context->audioq, &packet); } else { av_free_packet(&packet); } }
ここで、ファイルから次のパケットを読み取り、ストリームのタイプに応じて、デコードのために適切なキューに入れます。 これは逆多重化ストリームの主な機能であり、それ以上のアクションは実行されません。
ここで、より複雑なビデオおよびオーディオデコードストリームを検討します。
ビデオを再生する
ビデオのデコードと表示のプロセスについては、以前の記事で説明しました。 メインコードは変更されませんでしたが、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; // Set audio settings from codec info wanted_spec.freq = codec_context->sample_rate; wanted_spec.format = AUDIO_S16SYS; wanted_spec.channels = codec_context->channels; wanted_spec.silence = 0; wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE; wanted_spec.callback = audio_callback; wanted_spec.userdata = main_context; if (SDL_OpenAudio(&wanted_spec, &spec) < 0) { fprintf(stderr, "SDL: %s\n", SDL_GetError()); return -1; } SDL_PauseAudio(0);
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); // Get packet from queue AVPacket pkt; packet_queue_get(&main_context->audioq, &pkt, 1); // The audio packet can contain several frames int got_frame; int len = avcodec_decode_audio4(audio_stream->codec_context, &frame, &got_frame, &pkt); if (len < 0) { av_free_packet(&pkt); fprintf(stderr, "Failed to decode audio frame\n"); break; } if (got_frame) { // Store frame // Get decoded buffer size int data_size = av_samples_get_buffer_size(NULL, audio_stream->codec_context->channels, frame.nb_samples, audio_stream->codec_context->sample_fmt, 1); ring_buffer_write(&main_context->audio_buf, frame.data[0], data_size, 1); } av_free_packet(&pkt); } return 0; }
オーディオパケットは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) { /* if we have pts, set video clock to it */ main_context->video_clock = pts; } else { /* if we aren't given a pts, set it to the clock */ pts = main_context->video_clock; } /* update the video clock */ double frame_delay = av_q2d(video_codec_context->time_base); /* if we are repeating a frame, adjust clock accordingly */ 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 { /* if no pts, then compute it */ 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) { // Delay incorrect - use previous one delay = main_context->frame_last_delay; } // Save for next time main_context->frame_last_pts = main_context->pict.pts; main_context->frame_last_delay = delay; // Update delay to sync to audio double ref_clock = get_audio_clock(main_context); double diff = main_context->pict.pts - ref_clock; double sync_threshold = FFMAX(AV_SYNC_THRESHOLD, delay); if (fabs(diff) < AV_NOSYNC_THRESHOLD) { if (diff <= -sync_threshold) { delay = 0; } else if (diff >= sync_threshold) { delay = 2 * delay; } } main_context->frame_timer += delay; double actual_delay = main_context->frame_timer - (av_gettime() / 1000000.0); if(actual_delay < 0.010) { /* Really it should skip the picture instead */ actual_delay = 0.010; } return actual_delay; }
最初に、前のフレームと現在のフレームの間の遅延を計算し、現在の値を保存します。 その後、音声との非同期の可能性を考慮し、次のフレームまでに必要な遅延時間を計算します。
以上です! プレーヤーを起動し、視聴をお楽しみください!
おわりに
このパートでは、最も単純なプレーヤーの開発を完了し、プログラムの構造を改善し、オーディオの再生と同期を追加しました。
レビューされていないトピックのうち、巻き戻し、高速/低速再生、その他の同期オプションが残っています。
コーディングと多重化の完全に別のトピックがまだあります。 おそらく、次の記事でそれを検討しようと思います。
プレーヤーのソースコード 。
ご清聴ありがとうございました!