ビデオ プレーヤーの制御原理をゼロから理解する: ffplay プレーヤーのソース コード分析

ビデオ プレーヤーの原理は実際にはほぼ同じで、どちらもオーディオとビデオのフレーム シーケンスを制御します。ただし、一部のプレーヤーは、オーディオとビデオのより適切な同期を確保するために、オーディオとビデオの同期に、より複雑なフレーム予測テクノロジを使用する場合があります。

ffplay は、ビデオのレンダリングと表示に ffmpeg デコード ライブラリと sdl ライブラリを使用する FFMpeg の組み込みプレーヤーであり、業界のプレーヤーの最初のリファレンス デザイン標準でもあります。この記事では、ffplay のソース コードを分析し、より基本的で体系的な方法を使用して、プレーヤーのオーディオとビデオの同期、および再生/一時停止、早送り/巻き戻しの制御原理のロックを解除しようとします。

FFMpeg 自体のクロスプラットフォームの性質により、モバイル端末でオーディオ コードやビデオ コードを読み取るよりも、PC で VS を使用してコードを表示およびデバッグし、プレーヤーの原理を分析する方がはるかに効率的かつ高速です。

FFMpeg が公式に提供する ffmplay はコンソールで使用するには直感的ではないため、この記事では ffplay を VC に移植するコード (ffplay for MFC) を直接解析します。

この記事の特典として、無料の C++ オーディオおよびビデオ学習教材パッケージ、技術ビデオ/コード (オーディオおよびビデオの開発、インタビューの質問、FFmpeg、webRTC、rtmp、hls、rtsp、ffplay、コーデック、プッシュなど) を受け取ることができます。プルストリーミング、srs)↓↓↓ ↓↓↓下記からご覧ください↓↓無料で入手するには記事下をクリック↓↓

記事ディレクトリ:

  • 1. mp4 ファイルの予備調査
  • 2. 最も単純なプレーヤーから始めます: FFmpeg デコード + SDL 表示
  • 3. 最初に 5 つの質問をします
  • 4. ffplay コードの全体構造
  • 5.ビデオプレーヤーの動作制御
  • 6. ffplay コードの今回の分析に関する考察のまとめ

1. mp4 ファイルの予備調査

ビデオ ファイルについての予備的な理解を皆さんに提供するために、図 1 に示すように、まず MP4 ファイルの簡単な分析を見てみましょう。

図 1 MP4 ファイルのパラメータ解除

図 1 から、各ビデオ ファイルには特定のパッケージ化形式、ビット レート、再生時間、その他の情報があることがわかります。ビデオは逆多重化された後、video_stream と audio_stream に分割され、それぞれビデオ ストリームとオーディオ ストリームに対応します。

多重分離後の音声と映像はそれぞれ独立したパラメータを持ち、映像にはエンコード方式、サンプリングレート、ピクチャサイズなど、音声にはサンプリングレート、エンコード方式、チャンネル数などが含まれます。

逆多重化されたオーディオおよびビデオ パケットをデコードした後、元のオーディオ (PWM) およびビデオ (YUV/RGB) データになり、表示および再生できます。

実際、これでビデオのデコードと再生のプロセスの大部分がほぼカバーされており、ビデオ再生プロセス全体を図 2 に示します。

図 2 ビデオ再生プロセス

2. 最も単純なプレーヤーから始めます: FFmpeg デコード + SDL 表示

問題を単純化するために、今のところオーディオの再生は考慮せず、ビデオの再生のみを考えます。コード フローチャートを図 3 に示します。

図 3 プレーヤーのフローチャート (画像ソースの透かしを参照)

フローチャートの説明は次のとおりです。

1. FFmpeg 初期化のコードは比較的固定されており、主な目的は、AVFormatContext インスタンスに関連するメンバー変数の値を設定し、av_register_all、avformat_open_input、av_find_stream_info、avcodec_find_decoder などの関数を呼び出すことです。

図 4 に示すように、初期化後の AVFormatContext インスタンスの特定の値については、 av_find_stream_info を呼び出すことで、ファイル内のオーディオ ストリーム データとビデオ ストリーム データを検索し、ストリーム (オーディオ ストリームとビデオ ストリームを含む) 変数を初期化します。

図 4 AVFormatContext の初期化例

2. av_read_frame はストリーム内の次のフレームを継続的に読み取り、逆多重化してビデオの AVPacket を取得します。次に、avcodec_decode_video2 を呼び出してビデオ フレーム AVPacket をデコードし、画像フレーム AVFrame を取得します。

3. AVFrame を取得したら、次のステップは、レンダリングと表示のためにそれを SDL に入れることです。これも非常に簡単です。プロセスについては、以下のコード コメントを参照してください。

SDL_Overlay *bmp;
//将解析得到的AVFrame的数据拷贝到SDL_Overlay实例当中
SDL_LockYUVOverlay(bmp);
bmp->pixels[0]=pFrameYUV->data[0];
bmp->pixels[2]=pFrameYUV->data[1];
bmp->pixels[1]=pFrameYUV->data[2];    
bmp->pitches[0]=pFrameYUV->linesize[0];
bmp->pitches[2]=pFrameYUV->linesize[1];  
bmp->pitches[1]=pFrameYUV->linesize[2];

SDL_UnlockYUVOverlay(bmp);
//设置SDL_Rect,因为涉及到起始点和显示大小,用rect进行表示。
SDL_Rect rect;
rect.x = 0;   
rect.y = 0;   
rect.w = pCodecCtx->width; 
rect.h = pCodecCtx->height;   
//将SDL_Overlay数据显示到SDL_Surface当中。
SDL_DisplayYUVOverlay(bmp, &rect);
//延时40ms,留足ffmpeg取到下一帧并解码该帧的时间,随后继续读取下一帧
SDL_Delay(40);

上記の原理から、AVPacket がフレーム ストリームから取得され、デコードされて AVFrame が取得され、SDL ウィンドウにレンダリングされることがわかります。

図5 ビデオ再生ステータス図

ビデオ再生プロセスの概要は次のとおりです: 次のフレームの読み取り -> デコード -> 再生 -> 前後に続行します。状態図を図 5 に示します。

3. 最初に 5 つの質問をします

この記事では、オーディオとビデオのデコードと再生についての理解を深めるために、質問をして各問題の原理分析を徐々に行うという考え方を引き続き使用しています。以下の問題は、すべてのプレイヤーが直面する必要がある基本的な問題と原則でもあります。

1. 映画を見ると、その映画は中国語や英語の字幕や吹き替えなど、さまざまな字幕や音声に置き換えることができ、最終的には同じ画面に表示されることがわかります。字幕と音声は?実際、すべてのビデオ ファイルは読み出された後、異なるストリームに区別されます。誰もがより具体的に理解できるように、FFMpeg のコードを例として、AVMediaType は特定のストリーム タイプを定義します。

enum AVMediaType {

    AVMEDIA_TYPE_VIDEO,  //视频流

    AVMEDIA_TYPE_AUDIO, //音频流

    AVMEDIA_TYPE_SUBTITLE, //字幕流

};

av_read_frame を使用してオーディオ フレームとビデオ フレームを読み取った後、avcodec_decode_video2 を使用してビデオ Jetstar をデコードするか、avcodec_decode_audio4 を呼び出してオーディオをデコードして、レンダリングおよび表示できる元のオーディオ データとビデオ データを取得します。

画像と字幕は、Android の SurfaceFlinger と同様に、Surface またはテクスチャの形式になります。SurfaceFlinger は、画面のさまざまなモジュールの表示を組み合わせて新しい画像を生成し、ビデオ画面に表示します。

2. ビデオにはフレーム レート、オーディオにはサンプリング レートの概念があるため、フレーム レートを使用してオーディオとビデオの同期を直接制御できますか? 各ビデオ フレームとオーディオ フレームは時間領域の時点に対応しており、論理的に言えば、各オーディオ フレームとビデオ フレームの再生時間を制御することで同期を実現できます。

しかし実際には、各フレームの表示時間を正確に制御することは難しく、言うまでもなく、オーディオとビデオのデコード時間は異なるため、オーディオとビデオの同期がずれやすくなります。

では、プレーヤーはどのようにしてオーディオとビデオを同期するのでしょうか?

3. ビデオのオーディオ ストリーム、ビデオ ストリーム、字幕ストリームは時間的に連続していますか? それとも離散していますか? 異なるストリームのフレーム数は同じですか?

コンピューターは離散世界をデジタル的にシミュレートすることしかできないため、それらは時間的に離散的でなければなりません。では、離散型なのでフレーム番号は同じなのでしょうか?

ビデオは、多くのオーディオ フレーム、ビデオ フレーム、字幕フレームの時間的シーケンスとして理解できます。それらの時間長はビデオの合計長さと同じですが、各フレームのデコード時間は異なるため、必然的にそれらのフレームが発生します。時間間隔は同じではありません。

元のオーディオデータ自体はサンプリングされたデータであるため、クロック周期は一定です。ただし、映像と音声を同期させたい場合にはフレーム飛びが発生する場合があり、各映像フレームの再生時間差は一定の周期ではなく変動します。

結論から言えば、動画の合計時間の中で3人が再生するフレーム数は決定的に違うということになります。

4. ビデオ再生は、連続的にレンダリングされる一連の連続フレームです。ビデオ制御操作には、一時停止と再生、早送りと巻き戻しが含まれます。各早送り/巻き戻しの振幅が時間で測定されるのか、各ジャンプのフレーム数で測定されるのか、つまり、各早送りがどれだけ長く進むか、または何フレーム進むかについて考えたことはあります。時間とフレーム?

上記の問題の分析から、ビデオはオーディオ ストリーム、ビデオ ストリーム、字幕ストリームに分割されていることがわかりますが、フレーム数に基づく場合、異なるストリームのフレーム数は必ずしも同じではないため、フレーム数に応じて、ストリームの再生に一貫性のない 3 つの問題が発生する可能性があります。

したがって、mp4 ファイル ストリーム、つまり現在の再生時間の前方または後方の持続時間のシーク時点を直接検索し、ファイル ストリームを再分析してオーディオとビデオの同期を達成する尺度として時間を使用する方が比較的適切です。早送りと早戻し後の効果。

ほとんどのプレーヤーでは、早送り/巻き戻しが継続時間に基づいていることがわかります。ffplay がどのようなもので、どのように実装されているかがわかります。

5. 前のセクションでは、実装されたシンプルなプレーヤーはデコードと再生を同じスレッドで実行していましたが、デコード速度が再生速度に直接影響し、再生が滑らかでないという問題が直接発生します。では、デコード速度が不均一な場合にスムーズなビデオ再生を実現するにはどうすればよいでしょうか?

バッファ キューを導入し、ビデオ画像のレンダリングと表示とビデオ デコードを 2 つのスレッドとして使用することは簡単に考えられます。ビデオ デコード スレッドはキューにデータを書き込み、ビデオ レンダリング スレッドは表示のためにキューからデータを読み取ります。これにより、ビデオは処理でき、再生されます。

したがって、オーディオ フレーム、ビデオ フレーム、字幕フレームの 3 つのバッファ キューを使用する必要があります。

PTS はビデオ フレームまたはオーディオ フレームの表示タイムスタンプですが、ビデオ フレーム、オーディオ フレーム、字幕フレームの表示時間を制御するためにどのように使用されますか?

次に、ffplay がバッファ キュー制御をどのように行うかを調べます。

上記の 5 つの質問すべてについて、ffplay のソース コードを探索しながら、より具体的な答えを徐々に見つけていきます。

4. ffplay コードの全体構造

図 6 ffplay コードの全体的なフロー

図 6 に示すように、オンラインで誰かが ffplay の全体的なフローチャートを作成しました。この図を使用すると、コードがはるかに簡単に見えます。このプロセスに含まれる具体的な詳細は次のとおりです。

1. タイマーを開始します。タイマーは 40 ミリ秒ごとに更新され、SDL イベント メカニズムを使用して、レンダリングと表示のためにイメージ フレーム キューからのデータの読み取りをトリガーします。

2. stream_comonet_open 関数で、av_read_frame() が AVPacket を読み取り、オーディオ、ビデオ、または字幕のパケット キューに入れます。

3.video_thread は、ビデオ パケット キューから AVPacket を取得してデコードし、AVFrame 画像フレームを取得して、VideoPicture キューに入れます。

4..audio_thread スレッドは video_thread と同様に、オーディオ パケットをデコードします。

5.subtitle_thread スレッドは、video_thread と同様に、字幕パケットをデコードします。

5.ビデオプレーヤーの動作制御

動画プレーヤーの操作には、再生/一時停止、早送り/巻き戻し、コマ送り再生などが含まれますが、これらの操作の実装原理はどうなっているのでしょうか?コードレベルから一つ一つ分析してみましょう。

5.1 ffplay によって定義されるキー構造 VideoState

FFmpeg デコードと同様に、AVFormatContext 構造は、ファイル名、オーディオおよびビデオ ストリーム、グローバル アクセス用のデコーダなどのフィールドを格納するために定義されます。

ffplay は VideoState 構造体も定義しており、VideoState を分析することで、プレーヤーの基本的な実装原理を一般的に知ることができます。

typedef struct VideoState {
       // Demux解复用线程,读视频文件stream线程,得到AVPacket,并对packet入栈
       SDL_Thread *read_tid;  
       //视频解码线程,读取AVPacket,decode 爬出可以成AVFrame并入队
       SDL_Thread *video_tid;
       //视频播放刷新线程,定时播放下一帧
       SDL_Thread *refresh_tid;
       int paused;  //控制视频暂停或播放标志位
       int seek_req;  //进度控制标志
       int seek_flags;

       AVStream *audio_st;   //音频流
       PacketQueue audioq;  //音频packet队列
       double audio_current_pts;  //当前音频帧显示时间

       AVStream *subtitle_st; //字幕流
       PacketQueue subtitleq;//字幕packet队列 

       AVStream *video_st; //视频流

       PacketQueue videoq;//视频packet队列
       double video_current_pts; ///当前视频帧pts
       double video_current_pts_drift;  

       VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];  //解码后的图像帧队列
}

これは、VideoState 構造体から確認できます。

1. 逆多重化、ビデオデコード、およびビデオリフレッシュ再生は 3 つのスレッドに分割され、並列制御されます。

2. オーディオ ストリーム、ビデオ ストリーム、および字幕ストリームはすべて、異なるスレッドによる読み取りと書き込みのための独自のバッファ キューを持ち、現在のフレームの独自の PTS を持ちます。

3. デコードされた画像フレームは pictq キューに個別に配置され、SDL は表示にそれらを使用します。

PTSとはオーディオやビデオにおいて非常に重要な概念で、ビデオフレームやオーディオフレームの表示時間を直接決定するもので、以下で詳しく紹介します。

5.2 補足的な基礎知識 - PTS と DTS

この記事の特典として、無料の C++ オーディオおよびビデオ学習教材パッケージ、技術ビデオ/コード (オーディオおよびビデオの開発、インタビューの質問、FFmpeg、webRTC、rtmp、hls、rtsp、ffplay、コーデック、プッシュなど) を受け取ることができます。プルストリーミング、srs)↓↓↓ ↓↓↓下記からご覧ください↓↓無料で入手するには記事下をクリック↓↓

図 7 オーディオとビデオのデコード解析

図 7 は、出力オーディオ フレームとビデオ フレーム シーケンスを示しています。各フレームには PTS タグと DTS タグがあります。これら 2 つのタグは何を意味しますか? DTS (デコード タイム スタンプ) と PTS (プレゼンテーション タイム スタンプ) は両方ともタイムスタンプです。前者はデコード時間、後者は表示時間です。どちらも、上位層アプリケーションをより効果的にサポートするためのビデオ フレームとオーディオ フレームのタイム ラベルです。 . 同期メカニズム。

つまり、ビデオ フレームまたはオーディオがデコードされると、そのデコード時間が記録され、ビデオ フレームの再生時間は PTS に依存します。

サウンドの場合、これら 2 つのタイム タグは同じですが、一部のビデオ エンコード形式では、双方向予測テクノロジの使用により、DTS はオーディオとビデオの同期を確保するために特定のタイムアウトまたは遅延を設定し、これにより DTS と PTS が発生します。矛盾のこと。

5.3 オーディオとビデオの同期を制御する方法

ビデオ フレームの再生時間が実際に pts フィールドに依存することはすでにわかっています。オーディオとビデオには独自の個別の pts があります。しかし、ポイントはどのように生成されるのでしょうか?オーディオとビデオが同期していない場合、オーディオとビデオの同期を確保するためにポイントを動的に調整する必要がありますか?

まず、ビデオ フレームの表示時間を制御する方法を分析してみましょう。

static void video_refresh(void *opaque){ 

  //根据索引获取当前需要显示的VideoPicture
  VideoPicture *vp = &is->pictq[is->pictq_rindex];

  if (is->paused)
      goto display; //只有在paused的情况下,才播放图像

  // 将当前帧的pts减去上一帧的pts,得到中间时间差

  last_duration = vp->pts - is->frame_last_pts;

  //检查差值是否在合理范围内,因为两个连续帧pts的时间差,不应该太大或太小

  if (last_duration > 0 && last_duration < 10.0) {
    /* if duration of the last frame was sane, update last_duration in video state */
    is->frame_last_duration = last_duration;
  }

  //既然要音视频同步,肯定要以视频或音频为参考标准,然后控制延时来保证音视频的同步,
  //这个函数就做这个事情了,下面会有分析,具体是如何做到的。
  delay = compute_target_delay(is->frame_last_duration, is);

  //获取当前时间
  time= av_gettime()/1000000.0;

   //假如当前时间小于frame_timer + delay,也就是这帧改显示的时间超前,还没到,就直接返回
  if (time < is->frame_timer + delay) 
      return;

  //根据音频时钟,只要需要延时,即delay大于0,就需要更新累加到frame_timer当中。
  if (delay > 0)
       /更新frame_timer,frame_time是delay的累加值
       is->frame_timer += delay * FFMAX(1, floor((time-is->frame_timer) / delay));

  SDL_LockMutex(is->pictq_mutex);

  //更新is当中当前帧的pts,比如video_current_pts、video_current_pos 等变量
  update_video_pts(is, vp->pts, vp->pos);

  SDL_UnlockMutex(is->pictq_mutex);

display:
  /* display picture */
  if (!display_disable)
    video_display(is);
}

関数 compute_target_delay は、オーディオ クロック信号に基づいて遅延を再計算し、これによりオーディオに基づいてビデオの表示時間を調整し、オーディオとビデオの同期効果を実現します。

static double compute_target_delay(double delay, VideoState *is)
{
    double sync_threshold, diff;
   //因为音频是采样数据,有固定的采用周期并且依赖于主系统时钟,要调整音频的延时播放较难控制。所以实际场合中视频同步音频相比音频同步视频实现起来更容易。
   if (((is->av_sync_type == AV_SYNC_AUDIO_MASTER && is->audio_st) ||
     is->av_sync_type == AV_SYNC_EXTERNAL_CLOCK)) {

       //获取当前视频帧播放的时间,与系统主时钟时间相减得到差值
       diff = get_video_clock(is) - get_master_clock(is);
       sync_threshold = FFMAX(AV_SYNC_THRESHOLD, delay);

      //假如当前帧的播放时间,也就是pts,滞后于主时钟
      if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
         if (diff <= -sync_threshold)
             delay = 0;
      //假如当前帧的播放时间,也就是pts,超前于主时钟,那就需要加大延时
      else if (diff >= sync_threshold)
        delay = 2 * delay;
      }

   }
   return delay;
}

図 8 オーディオとビデオのフレーム表示シーケンス

ここでの処理は非常に簡単です. 図 8 はオーディオとビデオのフレームのシーケンスを単純に描画しています. 私が言いたいのは, オーディオのフレーム数とビデオのフレーム数は必ずしも等しいわけではないということです. また, の表示時間は各オーディオ フレームの時間はほぼ同じです。同様に、各ビデオ フレームの表示時間は、特定の状況に応じて遅延して表示されます。この遅延は、上記の compute_target_delay 関数によって計算されます。

遅延を計算した後、pts を更新するコードは次のようになります。

static void update_video_pts(VideoState *is, double pts, int64_t pos) {

    double time = av_gettime() / 1000000.0;
    /* update current video pts */
    is->video_current_pts = pts;
    is->video_current_pts_drift = is->video_current_pts - time;
    is->video_current_pos = pos;
    is->frame_last_pts = pts;
}

プロセス全体は次のように要約できます。

ビデオ画像の最初のフレームを表示します。

オーディオ信号に従って、2 番目のフレームの遅延時間を計算し、フレームのポイントを更新します。

pts が到着すると、ビデオ画像の 2 番目のフレームが表示されます。

最後のフレームまで上記の手順を繰り返します。

ここでもまだ混乱しているかもしれませんが、次のフレームの再生に必要な遅延がマスター クロックのみに基づいているのはなぜでしょうか?

実際には、ビデオは一定の長さの再生ストリームであり、オーディオ ストリーム、ビデオ ストリーム、字幕ストリームに分けられ、これら 3 つが同時に再生されてビデオが形成されます。動画ファイルの再生時間と同じです。

オーディオ ストリーム自体は PWM サンプリング データであるため、メイン クロックまたはその分周と同じ周波数である固定周波数で再生され、時間的には各オーディオ フレームが自然かつ均等に経過します。

したがって、オーディオの場合は、メインクロックまたはその周波数分割に従います。

ビデオは、独自の表示時間 (pts) に従ってメイン クロックの現在時刻と比較され、システム クロックより進んでいるか遅れているかを判断し、遅延を判断してから正確に再生する必要があります。オーディオとビデオの同期を確保します。

次に別の質問なのですが、遅延を計算した後、遅延表示のためにスリープする必要はありますか?

実際には、そうではありません。上記の分析から、更新する必要がある現在のビデオ フレームの pts (video_current_pts) に遅延が更新されることがわかります。現在の AVFrame を表示する前に、その pts 時間が最初に設定されます。未到着の場合は表示されず直接返送されます。次のリフレッシュまで、再検出します (ffplay は 40ms の定期リフレッシュを採用しています)。

コードは次のとおりです。更新された pts 時間 (is->frame_timer + dela) に達する前に、直接戻ります。

if (av_gettime()/1000000.0 < is->frame_timer + delay)  
    return;

次のステップはビデオ フレームの再生方法を分析することです。これは非常に簡単ですが、ここで追加の字幕ストリーム処理が追加されます。

static void video_image_display(VideoState *is)
{
    VideoPicture *vp;
   SubPicture *sp;
   AVPicture pict;
   SDL_Rect rect;
   int i;
   vp = &is->pictq[is->pictq_rindex];
   if (vp->bmp) {
       //字幕处理
       if (is->subtitle_st) {}                  
   }

   //计算图像的显示区域
   calculate_display_rect(&rect, is->xleft, is->ytop, is->width, is->height, vp);

   //显示图像
   SDL_DisplayYUVOverlay(vp->bmp, &rect);

   //将pic队列的指针向前移动一个位置
   pictq_next_picture(is);

}

VIDEO_PICTURE_QUEUE_SIZE は 4 にのみ設定されているため、すぐに使い果たされてしまいます。データがいっぱいになった場合、再度アップデートするにはどうすればよいですか?

キューサイズの制限を超えたことを検知すると、pictqを取り出して消費するまで待機状態となるため、大量のメモリを消費するプレーヤーの起動やファイル全体のデコードを回避できます。

static int queue_picture(VideoState *is, AVFrame *src_frame, double pts1, int64_t pos){

/* keep the last already displayed picture in the queue */
while (is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE - 2 &&
      !is->videoq.abort_request) {

    SDL_CondWait(is->pictq_cond, is->pictq_mutex);
   }
   SDL_UnlockMutex(is->pictq_mutex);
}

5.4 ビデオの再生と一時停止を制御するにはどうすればよいですか?

static void stream_toggle_pause(VideoState *is)
{

    if (is->paused) {
       //由于frame_timer记下来视频从开始播放到当前帧播放的时间,所以暂停后,必须要将暂停的时间( is->video_current_pts_drift - is->video_current_pts)一起累加起来,并加上drift时间。

     is->frame_timer += av_gettime() / 1000000.0 + is->video_current_pts_drift - is->video_current_pts;

     if (is->read_pause_return != AVERROR(ENOSYS)) {
     //并更新video_current_pts
        is->video_current_pts = is->video_current_pts_drift + av_gettime() / 1000000.0;

       }
    //drift其实就是当前帧的pts和当前时间的时间差
    is->video_current_pts_drift = is->video_current_pts - av_gettime() / 1000000.0;
    }

    //paused取反,paused标志位也会控制到图像帧的展示,按一次空格键实现暂停,再按一次就实现播放了。
    is->paused = !is->paused;
}

特記事項: 一時停止フラグは、ビデオを再生するかどうかを制御します。再生を継続する必要がある場合は、一時停止時間を追加する必要があるため、再生する必要がある現在のフレームの pts 時間を再更新する必要があります。

5.5 フレームごとの再生はどのように行われますか?

ビデオ デコード スレッドでは、stream_toggle_paused を継続的に使用してビデオの一時停止と表示を制御し、フレーム単位の再生を実現します。

static void step_to_next_frame(VideoState *is)
{
   //逐帧播放时,一定要先继续播放,然后再设置step变量,控制逐帧播放
   if (is->paused)
      stream_toggle_pause(is);//会不断将paused进行取反
   is->step = 1;
}

原理は、連続再生してから一時停止してフレームごとに再生することです。

static int video_thread(void *arg)
{
  if (is->step)
    stream_toggle_pause(is);
      ……………………
  if (is->paused)
    goto display;//显示视频
  }
}

5.6 早送りと巻き戻し

早送り/巻き戻しに関しては、まず 2 つの疑問が生じます。

1. 早送りは時間またはフレーム番号に基づいて再生の進行を制御しますか?

2. 進行状況が変化したら、現在のフレームと AVFrame キューをクリアする必要がありますか、またストリーム全体を再度制御する必要がありますか?

ffplayは時間を次元とした制御方式を採用しています。早送りや巻き戻しの制御はVideoStateのseek_reqやseek_posなどの変数を設定することで制御されます。

do_seek: //实际上是计算is->audio_current_pts_drift + av_gettime() / 1000000.0,确定当前需要播放帧的时间值 pos = get_master_clock(cur_stream); pos += incr; //incr为每次快进的步进值,相加即可得到快进后的时间点 stream_seek(cur_stream, (int64_t)(pos AV_TIME_BASE), (int64_t)(incrAV_TIME_BASE), 0); 关于stream_seek的代码如下,其实就是设置VideoState的相关变量,以控制read_tread中的快进或后退的流程:
/* seek in the stream */
static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
{

  if (!is->seek_req) {
  is->seek_pos = pos;
  is->seek_rel = rel;
  is->seek_flags &= ~AVSEEK_FLAG_BYTE;
  if (seek_by_bytes)
    is->seek_flags |= AVSEEK_FLAG_BYTE;
  is->seek_req = 1;
}
}

stream_seek で Seek_req フラグが設定されている場合、順方向/逆方向制御プロセスに直接入ります。原則として、avformat_seek_file 関数を呼び出してタイムスタンプに従ってインデックス ポイントを制御し、それによって表示する必要がある次のフレームを制御します。

static int read_thread(void *arg){
//当调整播放进度以后
if (is->seek_req) {
   int64_t seek_target = is->seek_pos;
   int64_t seek_min    = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
   int64_t seek_max    = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
  //根据时间抽查找索引点位置,定位到索引点之后,下一帧的读取直接从这里开始,就实现了快进/后退操作
  ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
  if (ret < 0) {
     fprintf(stderr, "s: error while seeking\n", is->ic->filename);
  } else {
  //查找成功之后,就需要清空当前的PAcket队列,包括音频、视频和字幕
     if (is->audio_stream >= 0) {
        packet_queue_flush(&is->audioq);
        packet_queue_put(&is->audioq, &flush_pkt);
     }
     if (is->subtitle_stream >= 0) {//处理字幕stream
        packet_queue_flush(&is->subtitleq);
        packet_queue_put(&is->subtitleq, &flush_pkt);
    }
    if (is->video_stream >= 0) {
       packet_queue_flush(&is->videoq);
       packet_queue_put(&is->videoq, &flush_pkt);
    }
  }
  is->seek_req = 0;
  eof = 0;
  }
}

さらに、上記のコードから、各早送りと巻き戻しの後に、audioq、videoq、subtitleq がフラッシュされてクリアされることがわかります。これは、バッファー キュー内のデータの正確性を確認するために最初からやり直すことと同じです。

オーディオに関しては、一時停止中にオーディオコントロールが表示されなかったため、最初は少し戸惑いました。

その後、is->paused 変数が実際には一時停止中に設定されていたことが判明し、逆多重化、オーディオのデコード、再生はすべて is->paused 変数に依存していたため、オーディオとビデオの再生が停止しました。

6. ffplay コードのこの分析に関する考察の要約:

1. 基本的な概念と原則の蓄積 私が初めて FFmpeg に触れたのですが、多くの概念が含まれているため、最初から始めようがないように思えます。このとき、基本的なモジュールから始めて、徐々に理解を深めていく必要があり、ある程度蓄積すると、質的な変化が生じ、ビデオのエンコードとデコードのメカニズムについての理解が深まります。

2. まずコードの全体的な構造とプロセスを理解してから、各詳細点を詳細に分析する必要があります。これにより、コードを読む効率が大幅に向上します。以下のようないくつかのブロック図を描けることが非常に重要であるため、詳細な UML 図よりも簡潔なフローチャートの方がはるかに便利です。

3. FFmpeg コードを確認し、PC 上でデバッグすると、はるかに高速になります。Android でコードを表示するために jni を呼び出したい場合、効率は非常に低くなります。

この記事の特典として、無料の C++ オーディオおよびビデオ学習教材パッケージ、技術ビデオ/コード (オーディオおよびビデオの開発、インタビューの質問、FFmpeg、webRTC、rtmp、hls、rtsp、ffplay、コーデック、プッシュなど) を受け取ることができます。プルストリーミング、srs)↓↓↓ ↓↓↓下記からご覧ください↓↓無料で入手するには記事下をクリック↓↓

おすすめ

転載: blog.csdn.net/m0_60259116/article/details/133133609