オーディオやビデオの学習は、直接見ることができ、よりユーザーに近い再生から始めるのが最善です。
オーディオおよびビデオ コーデックの基本
http、rtmp、またはローカルのビデオ ファイルを介してビデオを再生できます。ここでの「ビデオ」は、実際には、オーディオとビデオの両方のファイル形式を持つ mp4、avi を指します。
このようなビデオ ファイルには、ビデオ トラック、オーディオ トラック、サブタイトル トラックなどの複数のトラックが含まれる場合があります.一部の形式には、より多くの制限があります.たとえば、AVI ビデオ トラックは 1 つだけで、オーディオ トラックは 1 つだけです.一部のフォーマットはより柔軟です. 、たとえば、OGG ビデオには、複数のビデオおよびオーディオ トラックを含めることができます。
オーディオやビデオなどのデータ量の多いトラックの場合、上記のデータは実際に圧縮されます。ビデオ トラックは、H264 や H256 などの圧縮された画像データであり、デコードによって YUV、RGB などの形式の画像データに復元できます。オーディオ トラックは、デコードによって PCM オーディオ ストリームに復元できる MP3 や AAC などの圧縮オーディオ データである場合があります。
スクリーンショット 2022-09-04 pm 1.47.57.png
実際、ffmpeg を使用してビデオを再生するには、ファイル形式に従って段階的に画像データを復元し、それをディスプレイ デバイスに渡して表示し、オーディオ データをオーディオ デバイスに復元して再生します。
スクリーンショット 2022-09-04 pm 1.48.08.png
記事の最後の名刺には、(C/C++、Linux サーバー開発、FFmpeg、webRTC、rtmp、hls、rtsp、ffplay、srs) などのオーディオおよびビデオ開発学習資料と、オーディオおよびビデオ学習ロードマップを無料で受け取ることができます。など_
ffmpegの簡単な紹介
動画再生の流れを理解したら、簡単なプレーヤーを作って実際にffmpegを使ってみましょう。このブログは入門チュートリアルであるため、プレーヤー機能は簡略化されています。
-
ffmpeg 4.4.2 バージョンを使用 - 4.x バージョンが広く使用されており、最新の 5.x バージョンは情報が少ない
-
再生用に 1 つのビデオ トラックの画像のみをデコードします。オーディオとビデオの同期の問題を考慮する必要はありません。
-
SDL2 を使用してメイン スレッドでデコード - マルチスレッド同期の問題を考慮する必要はありません
-
ソース コード + Makefile を使用してビルド - MAC および Ubuntu で検証済み、Windows の学生は自分で vs プロジェクトを作成する必要があります
ffmpeg を使用して大まかにデコードするには、次の手順と主要な機能があり、上記のフローチャートに対応できます。
ファイル ストリームの解析 (プロトコル解除とカプセル化解除)
-
avformat_open_input : ファイルや RTMP などのプロトコルのデータ ストリームを開き、ファイル ヘッダーを読み取って、各トラックや期間などの解析など、ビデオ情報を解析することができます。
-
avformat_find_stream_info : MPEG や H264 ネイキッド ストリームなどのファイル ヘッダーのない形式の場合、この関数を使用して最初の数フレームを解析し、ビデオ情報を取得できます。
個々のトラックのデコーダーを作成する (ストリーミング)
-
avcodec_find_decoder: 対応するデコーダーを見つける
-
avcodec_alloc_context3: デコーダ コンテキストを作成する
-
avcodec_parameters_to_context: デコードに必要なパラメータを設定します
-
avcodec_open2: コーデックを開く
対応するデコーダーを使用して各トラックをデコード (デコード)
-
av_read_frame: ビデオ ストリームからビデオ データ パケットを読み取る
-
avcodec_send_packet: デコードのためにビデオ データ パケットをデコーダに送信します。
-
avcodec_receive_frame: デコーダーからデコードされたフレーム データを読み取ります
オーディオとビデオの部分に焦点を当てるために、デコード用の VideoDecoder クラスと画面表示用の SdlWindow クラスに分けて、主に VideoDecoder 部分に焦点を当てることができます。
ビデオストリーム分析
ファイル ストリームを解析し、実際にデコードする前にデコーダを作成するコードは比較的固定されているため、コードを直接投稿します. コメントをたどると、各ステップの意味を確認できます:
bool VideoDecoder::Load(const string& url) {
mUrl = url;
// 打开文件流读取文件头解析出视频信息如轨道信息、时长等
// mFormatContext初始化为NULL,如果打开成功,它会被设置成非NULL的值,在不需要的时候可以通过avcodec_free_context释放。
// 这个方法实际可以打开多种来源的数据,url可以是本地路径、rtmp地址等
// 在不需要的时候通过avformat_close_input关闭文件流
if(avformat_open_input(&mFormatContext, url.c_str(), NULL, NULL) < 0) {
cout << "open " << url << " failed" << endl;
return false;
}
// 对于没有文件头的格式如MPEG或者H264裸流等,可以通过这个函数解析前几帧得到视频的信息
if(avformat_find_stream_info(mFormatContext, NULL) < 0) {
cout << "can't find stream info in " << url << endl;
return false;
}
// 查找视频轨道,实际上我们也可以通过遍历AVFormatContext的streams得到,代码如下:
// for(int i = 0 ; i < mFormatContext->nb_streams ; i++) {
// if(mFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
// mVideoStreamIndex = i;
// break;
// }
// }
mVideoStreamIndex = av_find_best_stream(mFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if(mVideoStreamIndex < 0) {
cout << "can't find video stream in " << url << endl;
return false;
}
// 获取视频轨道的解码器相关参数
AVCodecParameters* codecParam = mFormatContext->streams[mVideoStreamIndex]->codecpar;
cout << "codec id = " << codecParam->codec_id << endl;
// 通过codec_id获取到对应的解码器
// codec_id是enum AVCodecID类型,我们可以通过它知道视频流的格式,如AV_CODEC_ID_H264(0x1B)、AV_CODEC_ID_H265(0xAD)等
// 当然如果是音频轨道的话它的值可能是AV_CODEC_ID_MP3(0x15001)、AV_CODEC_ID_AAC(0x15002)等
AVCodec* codec = avcodec_find_decoder(codecParam->codec_id);
if(codec == NULL) {
cout << "can't find codec" << endl;
return false;
}
// 创建解码器上下文,解码器的一些环境就保存在这里
// 在不需要的时候可以通过avcodec_free_context释放
mCodecContext = avcodec_alloc_context3(codec);
if (mCodecContext == NULL) {
cout << "can't alloc codec context" << endl;
return false;
}
// 设置解码器参数
if(avcodec_parameters_to_context(mCodecContext, codecParam) < 0) {
cout << "can't set codec params" << endl;
return false;
}
// 打开解码器,从源码里面看到在avcodec_free_context释放解码器上下文的时候会close,
// 所以我们可以不用自己调用avcodec_close去关闭
if(avcodec_open2(mCodecContext, codec, NULL) < 0) {
cout << "can't open codec" << endl;
return false;
}
// 创建创建AVPacket接收数据包
// 无论是压缩的音频流还是压缩的视频流,都是由一个个数据包组成的
// 解码的过程实际就是从文件流中读取一个个数据包传给解码器去解码
// 对于视频,它通常应包含一个压缩帧
// 对于音频,它可能是一段压缩音频、包含多个压缩帧
// 在不需要的时候可以通过av_packet_free释放
mPacket = av_packet_alloc();
if(NULL == mPacket) {
cout << "can't alloc packet" << endl;
return false;
}
// 创建AVFrame接收解码器解码出来的原始数据(视频的画面帧或者音频的PCM裸流)
// 在不需要的时候可以通过av_frame_free释放
mFrame = av_frame_alloc();
if(NULL == mFrame) {
cout << "can't alloc frame" << endl;
return false;
}
// 可以从解码器上下文获取视频的尺寸
// 这个尺寸实际上是从AVCodecParameters里面复制过去的,所以直接用codecParam->width、codecParam->height也可以
mVideoWidth = mCodecContext->width;
mVideoHegiht = mCodecContext->height;
// 可以从解码器上下文获取视频的像素格式
// 这个像素格式实际上是从AVCodecParameters里面复制过去的,所以直接用codecParam->format也可以
mPixelFormat = mCodecContext->pix_fmt;
return true;
}
VideoDecoder::Load を使用してビデオ ストリームを開き、デコーダを準備します。After that is the decode process. デコードが完了したら、VideoDecoder::Release を呼び出してリソースを解放します。
void VideoDecoder::Release() {
mUrl = "";
mVideoStreamIndex = -1;
mVideoWidth = -1;
mVideoHegiht = -1;
mDecodecStart = -1;
mLastDecodecTime = -1;
mPixelFormat = AV_PIX_FMT_NONE;
if(NULL != mFormatContext) {
avformat_close_input(&mFormatContext);
}
if (NULL != mCodecContext) {
avcodec_free_context(&mCodecContext);
}
if(NULL != mPacket) {
av_packet_free(&mPacket);
}
if(NULL != mFrame) {
av_frame_free(&mFrame);
}
}
ビデオデコード
デコーダーが作成されたら、デコードを開始できます。
AVFrame* VideoDecoder::NextFrame() {
if(av_read_frame(mFormatContext, mPacket) < 0) {
return NULL;
}
AVFrame* frame = NULL;
if(mPacket->stream_index == mVideoStreamIndex
&& avcodec_send_packet(mCodecContext, mPacket) == 0
&& avcodec_receive_frame(mCodecContext, mFrame) == 0) {
frame = mFrame;
... //1.解码速度问题
}
av_packet_unref(mPacket); // 2.内存泄漏问题
if(frame == NULL) {
return NextFrame(); // 3.AVPacket帧类型问题
}
return frame;
}
そのコア ロジックは、実際には次の 3 つのステップです。
-
av_read_frame を使用してビデオ ストリームからビデオ パケットを読み取る
-
avcodec_send_packet を使用して、デコードのためにビデオ パケットをデコーダに送信します。
-
avcodec_receive_frame を使用して、デコーダからデコードされたフレーム データを読み取ります
重要な 3 つの手順に加えて、注意すべき点がいくつかあります。
1. デコード速度の問題
デコード速度は比較的速いため、デコードする前に次のフレームを再生する必要があるまで待つことができます。これにより、CPU 使用率を削減できます。また、描画スレッドが画面キューをスタックすることによって引き起こされる高いメモリ使用率も削減できます。
このデモには別のデコード スレッドがないため、レンダリング スレッドでのデコード、sdl レンダリング自体に時間がかかるため、遅延がなくても通常の速度で画像が再生されることがわかります。描画コードを作成し、このメソッドを追加してインターネットで印刷すると、ビデオ全体が一度にデコードされていることがわかります。
2.メモリリークの問題
デコードが完了すると、圧縮されたデータ パケットのデータは不要になるため、av_packet_unref を使用して AVPacket を解放する必要があります。
実際には、AVFrame も使用後に AVFrame のピクセル データを解放するために av_frame_unref を使用する必要がありますが、avcodec_receive_frame で av_frame_unref が呼び出されて前のフレームのメモリがクリアされ、最後のフレームのデータも av_frame_free によってクリアされます。したがって、手動で av_frame_unref を呼び出す必要はありません。
3. AVPacket フレームタイプの問題
ビデオ圧縮フレームには i フレーム、b フレーム、p フレームなどの種類があるため、すべてのフレームが元の画像を直接デコードできるわけではありません。現在のフレームと前後のフレーム. 違いは、次のフレームをデコードする必要があることです.
AVPacket のこのフレームがデータをデコードしない場合、元の画像の次のフレームがデコードされるまで、NextFrame を再帰的に呼び出して次のフレームをデコードします。
PTS同期
AVFrame には pts メンバ変数があり, いつ画像を表示するかを表します. ビデオのデコード速度は通常非常に速いため, たとえば, 1 分間のビデオは 1 秒でデコードされることがあります. したがって, このフレームをいつ計算する必要がありますか?時間が経過していない場合は遅延を追加して再生します。
一部のビデオ ストリームには pts データがなく、各フレーム間の間隔は 30fps に従って 32ms に統一されます。
if(AV_NOPTS_VALUE == mFrame->pts) {
int64_t sleep = 32000 - (av_gettime() - mLastDecodecTime);
if(mLastDecodecTime != -1 && sleep > 0) {
av_usleep(sleep);
}
mLastDecodecTime = av_gettime();
} else {
...
}
ビデオ ストリームに pts データがある場合、pts がビデオに含まれるマイクロ秒数を計算する必要があります。
pts の単位は、AVFormatContext を介して対応する AVStream を見つけ、AVStream の time_base を取得できます。
AVRational timebase = mFormatContext->streams[mPacket->stream_index]->time_base;
AVRational是个分数,代表几分之几秒:
/**
* Rational number (pair of numerator and denominator).
*/
typedef struct AVRational{
int num; ///< Numerator
int den; ///< Denominator
} AVRational;
timebase.num * 1.0f / timebase.den を使用してこの分数の値を計算し、1000 を掛けて ms まで待機し、1000 を掛けて取得します. 計算の後半は実際に VideoDecoder に保存できます::メンバー変数にロードしますが、説明の便宜上、ここに配置します:
int64_t pts = mFrame->pts * 1000 * 1000 * timebase.num * 1.0f / timebase.den;
この pts はビデオの先頭から計算されるため、最初のフレームのタイムスタンプを最初に保存してから、現在再生されているマイクロ秒数を計算する必要があります. 完全なコードは次のとおりです:
if(AV_NOPTS_VALUE == mFrame->pts) {
...
} else {
AVRational timebase = mFormatContext->streams[mPacket->stream_index]->time_base;
int64_t pts = mFrame->pts * 1000 * 1000 * timebase.num * 1.0f / timebase.den;
// 如果是第一帧就记录开始时间
if(mFrame->pts == 0) {
mDecodecStart = av_gettime() - pts;
}
// 当前时间减去开始时间,得到当前播放到了视频的第几微秒
int64_t now = av_gettime() - mDecodecStart;
// 如果这一帧的播放时间还没有到就等到播放时间到了再返回
if(pts > now) {
av_usleep(pts - now);
}
}
他の
完全なデモがGithub に置かれています. 画像レンダリング部分は SdlWindow クラスにあります. SDL2 を使用して UI 描画を行っています. オーディオおよびビデオ コーデックとは関係がないため、ここでは説明しません. ビデオ デコード一部は VideoDecoder クラスにあります。
コンパイルするときは、Makefile 内の ffmpeg と sdl2 のパスを変更し、make のコンパイル後に次のコマンドを使用してビデオを再生する必要があります。
demo -p ビデオ パス ビデオを再生
PS:
一部の関数には、avcodec_alloc_context3、avcodec_open2 などの番号サフィックスがあります。実際、この番号サフィックスは、ソース コードの doc/APIchanges からわかるように、この関数の最初のバージョンを意味します。
2011-07-10 - 3602ad7 / 0b950fe - lavc 53.8.0
Add avcodec_open2(), deprecate avcodec_open().
NOTE: this was backported to 0.7
Add avcodec_alloc_context3. Deprecate avcodec_alloc_context() and
avcodec_alloc_context2().