基本的なことについて
マルチプレクサの最も難しい点はタイムスタンプの概念であり、これに関連する基本的な概念をいくつか次に示します。
サンプリングレート
現在、オーディオとビデオで使用されているデジタル コーディング方法は、オーディオとビデオの波形と画像を収集、量子化、エンコード、送信、デコードするだけです。したがって、サンプリング レートは、1 秒あたりに画像または音波の振幅サンプルが取得される回数です。たとえば、オーディオのサンプリング レートは 8k で、これは波形が 1 秒あたり 8000 回サンプリングされることを意味します。
1 秒というサンプリング周波数が実際にはかなり高いことがわかりますが、この値がどの程度妥当であるかというと、実は映像も音声も人間の視覚や聴覚の特性に関係しています。
人間の視覚では、1秒間に再生される動画が25フレーム以上であれば、連続した画像が動画として認識されます。この値よりも低い場合、人間の目は遅れを感じることができます。
人間の聴覚の通常の可聴周波数範囲は 20Hz ~ 20kHz ですが、クエストのサンプリング理論によれば、音声が歪まないようにするには、サンプリング周波数は約 40kHz である必要があります。サンプリング レートが高いほど良いわけではないのはなぜですか? サンプリング レートが高いほど送信するデータが多くなり、エンコードと送信に大きな負担がかかり、コストも重要な考慮事項になるからです。
フレームレート
フレーム レートは 1 秒間に表示されるフレーム数です。たとえば、30fps は 1 秒間に 30 フレームの画像が表示されることを意味します。しかし、音声のフレームレートは少し抽象的で理解しにくいです。オーディオの場合、AAC や mp3 などのエンコード方式ではそれぞれ 1024 サンプルが規定されており、mp3 の各フレームは 1152 サンプルとなりますが、サンプルを 1 バイトで表すと、つまり AAC エンコードされたオーディオの 1024 バイトが 1 フレームとなり、1152 バイトとなります。 MP3 エンコード モードのオーディオの 1 フレームです。
タイムスタンプ単位
先ほどサンプリングレートについて触れましたが、サンプリングレートは非常に大きな単位であると感じますが、一般的な標準的な音声AACのサンプリングレートは44kHzに達し、映像のサンプリングレートも90000Hzと規定されており、時間を計測する単位ではあり得ません。リアルタイムの単位は、サンプリング レートに変換する必要があります。つまり、サンプリング時間はオーディオとビデオの時間単位であり、タイムスタンプの実際の値です。再生および制御したい場合は、サンプリング レートに従ってタイムスタンプをリアルタイムに変換できます。
つまり、タイムスタンプはリアルタイムではなく、サンプル数です。たとえば、タイムスタンプが 160 の場合、それが 160 秒または 160 ミリ秒であるとは考えられず、160 サンプルである必要があります。リアルタイムを変換するには、8000 などのサンプリング レートを知る必要があります。その場合、1 秒が 1/8000 に分割されることを意味します。160 サンプルにかかる時間を知りたい場合は、160*(1/8000)つまり 20 ミリ秒あれば十分です。
タイムスタンプの増分
これは、画像のあるフレームと画像の別のフレームの間のタイムスタンプの差、または音声のあるフレームと音声のあるフレームの間のタイムスタンプの差です。同様に、タイムスタンプの増分もサンプル数の差ですが、これはリアルタイムの差ではなく、サンプリング レートに基づいてのみリアルタイムに変換できます。
したがって、ビデオとオーディオのタイムスタンプ計算では、フレーム レートとサンプリング レートが明確である必要があります。
たとえば、ビデオの場合、フレーム レートは 25 であるため、サンプリング レート 90000 の場合、1 フレームが占めるサンプル数は 90000/25、つまり 3600 になります。これは、画像の各フレームのタイムスタンプの増分が必要であることを示しています。実際の時間に換算すると 3600 となり、3600*(1/90000)=0.04 秒=40 ミリ秒となり、1/25=0.04 秒=40 ミリ秒とも一致します。
AAC オーディオの場合、1 フレームに 1024 個のサンプルがあり、サンプリング周波数は 44kHz であるため、1 フレームの再生時間は 1024 * (1/44100) = 0.0232 秒 = 23.22 ミリ秒になります。
同期方法:
上で述べたように、タイムスタンプの重要な機能はオーディオとビデオを同期させることですが、このタイムスタンプはどのようにしてオーディオとビデオを同期させるのでしょうか?
プレーヤーは、システム クロックをローカルに確立する必要があります。このクロックは通常、CPU 時間に基づいて計算されます。再生が開始されるとき、クロック時間は 0 で、タイムスタンプによってフレームがデコードおよびレンダリングされる瞬間が決まります。再生が開始されると、クロック時間が増加し、プレーヤーはシステム クロックと現在のビデオおよびオーディオのタイムスタンプを比較します。オーディオおよびビデオのタイムスタンプが現在のシステム クロックより小さい場合は、デコードとレンダリングの再生を理解する必要があります。
正確に再生できるかどうかは、エンコーダーのタイムスタンプが正確である必要があり、プレーヤー側のシステムクロックも正確である必要があります。これは、タイムスタンプとデータストリームに基づいてデータストリームを制御する必要があるためです。再生時のシステムクロック、つまりデータブロックが正確である必要があり、タイムスタンプに応じて異なる処理方法が採用されます。実際、エンコーダもローカル プレーヤーもそれほど正確ではないため、固定フレーム レートは 25 であると言われていますが、エンコーダが一度に 24 フレームを再生する可能性もあります。この累積エラーの問題を解決するには、一般に、このエラーを除去するために再生側に一連のフィードバック メカニズムが必要です。実際、同期は動的なプロセスであり、誰かが待機し、誰かが追いつくプロセスです。同期は一時的なものにすぎず、非同期が標準です。人間は常に同期した水平線に沿って振動し、変動しますが、このベースラインから大きく逸脱することはありません。
PTS と DTS:
上で紹介した基本的な考え方は、実際の使用過程でタイムスタンプのPTSとDTSの表現を導き出すというもので、このうちDTSはDecoding Time Stampであり、デコード時のタイムスタンプであり、このタイムスタンプの意味がプレイヤーにいつであるかを知らせるものである。このフレームのデータをデコードします。
PTS (プレゼンテーション タイムスタンプ) は、このフレームのデータをいつ表示するかをプレーヤーに伝えるために使用されるタイムスタンプを表示します。
これら 2 つのタイムスタンプはプレーヤーの動作をガイドするために使用されますが、どちらもエンコーダーによって生成されます。通常、フレームをデコードしたらすぐに再生する必要がありますが、いつデコードするか、いつ再生するかについては、1 つのタイムスタンプで判断できますが、なぜ今 2 つのタイムスタンプが導入されているのでしょうか。もちろん、ここで説明する DTS と PTS はビデオ用です。ビデオには 2 つのタイムスタンプのみが使用され、オーディオには 1 つのタイムスタンプが使用されるためです。つまり、プレーヤーはオーディオのタイムスタンプに達するとすぐにオーディオをデコードして再生し、途中で遅延が発生することはありません。ビデオがより複雑になる理由は、ビデオに IP B という 3 種類のフレームがあるためです。
I フレーム: イントラ符号化されたフレーム (イントラ ピクチャとも呼ばれる) I フレームは通常、各 GOP の最初のフレームであり、フレーム内圧縮を採用し、ランダム アクセスの基準点として適度に圧縮され、どのフレームからも独立して使用できます。フレームをデコードして表示します。最大量のデータは圧縮された画像として表示されます。
P フレーム: 予測フレームとも呼ばれる前方予測符号化フレームは、画像シーケンス内の以前の符号化フレームよりも低い時間冗長情報を完全に圧縮することによって、送信データの符号化画像を圧縮します。符号化にはフレーム間予測技術が使用されます。
B フレーム: 双方向予測内挿フレームであり、双方向内挿フレームとも呼ばれ、P フレームと比較して、前のフレームに依存し、後続の P フレームにも依存してフレーム間通信を利用します。
データを圧縮するための冗長な情報。
上記比較より、フレーム圧縮率はBフレーム>Pフレーム>Iフレームとなり、データ量はその逆になります。
B フレームがない場合、送信されたビデオ フレームが IPPP であると仮定すると、各フレームのタイムスタンプに従ってデコードして表示できます。後続のフレームのタイムスタンプは常に前のタイムスタンプより大きいため、タイムスタンプを使用します。それがカンです。しかし、B フレームでは、デコードと表示が複雑になります。
-
実際にフレームを表示する順序は、IBBP フレームがデコードされる順序です。
-
しかし実際には、これらのフレームが到着した後、I フレームと B フレームの特性に従って、キャッシュ内の実際の順序は次のようになります。
-
実際のデコードシーケンス: 1 4 2 3;
-
最終的なプレゼンテーションの順序は次のとおりです: 1 2 3 4;
つまり、最初に I フレームが再生され、次に最初の B フレーム、2 番目の B フレーム、最後に P フレームが再生されます。
まとめると、上記のフレームのデコード順序と再生表示順序が矛盾していることがわかりますが、これらをいつデコードし、いつ再生するかを制御するために、DTS と PTS の概念が確立されました。デコードされたフレームには PTS の概念のみがあり、デコードされていないフレームには DTS と PTS の概念があることに注意してください。
Iフレームの場合、PTS=DTS、PフレームのPTS>DTS、BフレームのPTS<DTSとなる。もちろん、ここでの「以上」と「以下」は BP フレームに対するものです。タイムスタンプが小さい場合は最初にデコードまたは表示が行われることを意味し、値が大きい場合は後処理を意味します。
マルチプレクサコードの実装
#include <stdio.h>
#include <iostream>
extern "C"
{
#include "libavformat/avformat.h"
};
int main(int argc, char* argv[])
{
AVFormatContext* ifmtCtxVideo = NULL, * ifmtCtxAudio = NULL, * ofmtCtx = NULL;
AVCodecContext* video_ctx = NULL;
AVPacket packet;
AVCodec* video_codec = NULL;
//AVBSFContext* bsf_ctx = nullptr;
const AVBitStreamFilter* pfilter = av_bsf_get_by_name("h264_mp4toannexb");
AVBitStreamFilterContext* h264bsfc = av_bitstream_filter_init("h264_mp4toannexb");
//av_bsf_alloc(pfilter, &bsf_ctx);
int inVideoIndex = -1, inAudioIndex = -1;
int outVideoIndex = -1, outAudioIndex = -1;
int audioindex = 0;
int videoindex = 0;
int64_t curPstVideo = 0, curPstAudio = 0;
int ret = 0;
unsigned int i = 0;
const char* inFilenameVideo = "Titanic.h264";
const char* inFilenameAudio = "Titanic.aac";
const char* outFilename = "test.mp4";
//打开输入视频文件
ret = avformat_open_input(&ifmtCtxVideo, inFilenameVideo, 0, 0);
if (ret < 0)
{
printf("can't open input video file\n");
goto end;
}
//查找输入流
ret = avformat_find_stream_info(ifmtCtxVideo, 0);
if (ret < 0)
{
printf("failed to retrieve input video stream information\n");
goto end;
}
//打开输入音频文件
ret = avformat_open_input(&ifmtCtxAudio, inFilenameAudio, 0, 0);
if (ret < 0)
{
printf("can't open input audio file\n");
goto end;
}
//查找输入流
ret = avformat_find_stream_info(ifmtCtxAudio, 0);
if (ret < 0)
{
printf("failed to retrieve input audio stream information\n");
goto end;
}
printf("===========Input Information==========\n");
av_dump_format(ifmtCtxVideo, 0, inFilenameVideo, 0);
av_dump_format(ifmtCtxAudio, 0, inFilenameAudio, 0);
printf("======================================\n");
//新建输出上下文
avformat_alloc_output_context2(&ofmtCtx, NULL, NULL, outFilename);
if (!ofmtCtx)
{
printf("can't create output context\n");
goto end;
}
//视频输入流
for (i = 0; i < ifmtCtxVideo->nb_streams; ++i)
{
if (ifmtCtxVideo->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
AVStream* inStream = ifmtCtxVideo->streams[i];
AVStream* outStream = avformat_new_stream(ofmtCtx, NULL);
//av_dump_format(ofmtCtx, 0, outFilename, 1);
inVideoIndex = i;
if (!outStream)
{
printf("failed to allocate output stream\n");
goto end;
}
outVideoIndex = outStream->index;
if (avcodec_parameters_copy(outStream->codecpar, inStream->codecpar) < 0)
{
printf("faild to copy context from input to output stream");
goto end;
}
outStream->codecpar->codec_tag = 0;
//av_dump_format(ofmtCtx, 0, outFilename, 1);
break;
}
}
// 解码器解码
video_ctx = avcodec_alloc_context3(video_codec);
video_codec = avcodec_find_decoder(ifmtCtxVideo->streams[0]->codecpar->codec_id);
video_ctx = ifmtCtxVideo->streams[0]->codec;
//音频输入流
for (i = 0; i < ifmtCtxAudio->nb_streams; ++i)
{
if (ifmtCtxAudio->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{
AVStream* inStream = ifmtCtxAudio->streams[i];
AVStream* outStream = avformat_new_stream(ofmtCtx, NULL);
inAudioIndex = i;
if (!outStream)
{
printf("failed to allocate output stream\n");
goto end;
}
if (avcodec_parameters_copy(outStream->codecpar, inStream->codecpar) < 0)
{
printf("faild to copy context from input to output stream");
goto end;
}
outAudioIndex = outStream->index;
break;
}
}
printf("==========Output Information==========\n");
av_dump_format(ofmtCtx, 0, outFilename, 1);
printf("======================================\n");
//打开输入文件
if (!(ofmtCtx->oformat->flags & AVFMT_NOFILE))
{
if (avio_open(&ofmtCtx->pb, outFilename, AVIO_FLAG_WRITE) < 0)
{
printf("can't open out file\n");
goto end;
}
}
//写文件头
if (avformat_write_header(ofmtCtx, NULL) < 0)
{
printf("Error occurred when opening output file\n");
goto end;
}
while (1)
{
AVFormatContext* ifmtCtx = NULL;
AVStream* inStream, * outStream;
int streamIndex = 0;
if (av_compare_ts(curPstVideo, ifmtCtxVideo->streams[inVideoIndex]->time_base, curPstAudio, ifmtCtxAudio->streams[inAudioIndex]->time_base) < 0)
{
ifmtCtx = ifmtCtxVideo;
streamIndex = outVideoIndex;
if (av_read_frame(ifmtCtx, &packet) >= 0)
{
//printf("Video start packet.pts = %d \n\n", packet.pts);
inStream = ifmtCtx->streams[packet.stream_index];
//printf("Video sample_rate = %\n", inStream->codecpar->sample_rate);
outStream = ofmtCtx->streams[streamIndex];
if (packet.stream_index == inVideoIndex)
{
av_bitstream_filter_filter(h264bsfc, ifmtCtxVideo->streams[0]->codec, NULL, &packet.data, &packet.size, packet.data, packet.size, 0);
// Fix: No PTS(Example: Raw H.264
// Simple Write PTS
if (packet.pts == AV_NOPTS_VALUE)
{
//write PTS
AVRational timeBase1 = inStream->time_base;
//Duration between 2 frames
double calcDuration = (double)1.0 / av_q2d(inStream->r_frame_rate);
//Parameters 转化为
printf("Video calcDuration = %lf\n", calcDuration);
packet.pts = (double)(videoindex * calcDuration) / (double)(av_q2d(timeBase1));
packet.dts = packet.pts;
packet.duration = (double)calcDuration / (double)(av_q2d(timeBase1));
videoindex++;
//printf("Video PTS: %ld\n", packet.pts);
//printf("Video DTS: %ld\n", packet.dts);
}
curPstVideo = packet.pts;
}
}
else
{
break;
}
}
else
{
ifmtCtx = ifmtCtxAudio;
streamIndex = outAudioIndex;
if (av_read_frame(ifmtCtx, &packet) >= 0)
{
//printf("Audio start packet.pts = %d \n\n", packet.pts);
inStream = ifmtCtx->streams[packet.stream_index];
outStream = ofmtCtx->streams[streamIndex];
if (packet.stream_index == inAudioIndex)
{
//Fix: No PTS(Example: Raw H.264
//Simple Write PTS
AVRational timeBase1 = inStream->time_base;
//Duration between 2 frames
double calcDuration = (double)1024.0 / inStream->codecpar->sample_rate;
printf("Audio calcDuration = %lf\n", calcDuration);
packet.pts = (double)(audioindex * calcDuration) / (double)(av_q2d(timeBase1));
packet.dts = packet.pts;
packet.duration = (double)calcDuration / (double)(av_q2d(timeBase1));
audioindex ++;
//printf("Audio PTS: %ld\n", packet.pts);
//printf("Audio DTS: %ld\n", packet.dts);
curPstAudio = packet.pts;
}
}
else
{
break;
}
}
//FIX:Bitstream Filter
//Convert PTS/DTS
packet.pts = av_rescale_q_rnd(packet.pts, inStream->time_base, outStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
packet.dts = av_rescale_q_rnd(packet.dts, inStream->time_base, outStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
packet.duration = av_rescale_q(packet.duration, inStream->time_base, outStream->time_base);
packet.pos = -1;
packet.stream_index = streamIndex;
//write
printf("Audio sample_rate = %d \n\n", inStream->codecpar->sample_rate);
if (av_interleaved_write_frame(ofmtCtx, &packet) < 0)
{
printf("error muxing packet");
break;
}
av_packet_unref(&packet);
}
av_write_trailer(ofmtCtx);//写文件尾
end:
avformat_close_input(&ifmtCtxVideo);
avformat_close_input(&ifmtCtxAudio);
if (ofmtCtx && !(ofmtCtx->oformat->flags & AVFMT_NOFILE))
avio_close(ofmtCtx->pb);
avformat_free_context(ofmtCtx);
return 0;
}