オーディオおよびビデオプレーヤー(ffmpeg3.2 + SDL2.0、映像+音声)を開発する方法
序文
前のオーディオプレーヤーを開発する方法について話しました、実際には非常に簡単です。フレームからオーディオファイルを取得し、その後、オーディオデータを直接読み込むとオーディオのプロセスを再生するコールバックシステムのシステムに渡される以上のものを合計しないように。
我々は近い「ビデオ+オーディオ」、「メディアプレーヤーを開発する、とこのチュートリアルでのステップ。この時間は、強力な連続性が、上の読み、この1を見てください。また、この時使用しました独自のバージョンは、GitHubの上Benpian ffmpeg3.2提出されたソースコードの使用を注意してください読みながら+ SDL2.0はまだ、開発している:. https://github.com/XP-online/media-player
ビデオやオーディオプレーヤーを作成する手順
ビデオやオーディオプレーヤーを作成する方法冷蔵庫の問題で象に似ています。これは3つのステップの合計である、すなわち:再生中のオーディオ - ビデオ再生する - オーディオとビデオの同期を。
まず、オーディオを再生
我々はについて話しました記事のこの部分。
第二に、ビデオを再生
第二段階では、それは2つの部分に分割された映像を再生するには、おそらく次のとおりです。
- ファイルのビデオフレームを読みます
- 情報取得された画像によると、ビデオを再生します
ほとんど差オーディオフレーム方法の第1の部分を読み取ります。しかし、彼らはビデオとオーディオの再生を再生方法の第二段階は完全に異なっています。ここで終了しますが、多くの方法は、画像のリアリティがあり、それが原因で、既に取得したすべてのビデオ画像情報のも十分理解されています。
実際には、我々は何の問題をレンダリングしないように、自分のビデオ画像情報に応じて他のGUIライブラリに画像情報を取得した後、SDLで遊ぶためにここに適用することはできません。しかしBenpian種我々の焦点は、メディアファイルの再生の重要なステップを導入することで、我々はまだビデオをレンダリングするためにSDL2.0を使用し、原則として他のライブラリの使用を最小限に保持しています。
第三に、オーディオとビデオの同期
映像と音声の再生、独立してデコードされているため。それらの各デコーダに、異なるレンダリング率が同期して映像と音声のケースを持っている可能性があります。そんなに早くその当事者がこの問題を解決するために、多くの場合、低速側を待つ必要があります。これは、オーディオとビデオの同期の問題です。幸いなことに、ffmpegのパッケージ(*によって取得した各AVPacket *)は、現在の時刻情報パケットで封止されている:PTSとDTSを。我々は後で説明します彼らの特定の意味は、単にこれら二つの部材を流れる電流が、我々は現在のパケットのタイムスタンプをすることができ、時間を得ることができることを知っています。
一般的なオーディオとビデオの同期、すなわち三つの主要な方法があります。
- ベンチマークとしてオーディオタイムスタンプ。ビデオは、現在のパッケージを放棄するか、再生またはオーディオタイムスタンプを待つことにしました。
- ベンチマークとしてのビデオタイムスタンプ。オーディオは、現在のパッケージがまだ再生されて放棄またはビデオのタイムスタンプを待つことにしました。
- ベンチマークとしてシステムにタイムスタンプ。オーディオおよびビデオプレーヤーが現在のパッケージを決めるか、あきらめるか、システム時間に応じて待ちます。
第1のアプローチでは、このオーディオのタイムスタンプの対象とチュートリアル、すなわち、オーディオビデオ同期。
ソースコード解析
よると、私たちに述べた上で、再生中のオーディオおよびビデオの再生が独立し、互いに分離するので、我々は2つのスレッドが、オーディオおよびビデオ再生するにして開く必要がする必要があるとき。同時に、私たちの前にケースは音声のみを再生するとは異なり、ビデオパケットは、私たちがこれを行うことができないことが明らかに前に廃棄されています。だから我々はまた、保存するために、オーディオとビデオのバッファキューを作成する必要がパッケージに読み込まれており、その後、パッケージを介してビデオとオーディオの再生キューのスレッドに送られます。ここでは、マルチスレッドの動作があるので、2つのキューは、スレッドセーフでなければなりません。
公式の開始前にまず、準備作業
最初の公式のソースコード解析の前に、スレッドセーフなキューを作成
#include <vector>
#include <mutex>
template<typename T>
// 线程安全的队列
class Queue
{
public:
Queue() {
q.clear();
}
// push 入列
void push(T val) {
m.lock();
q.push_back(val);
m.unlock();
}
// pull 出列,返回false说明队列为空
bool pull(T &val) {
m.lock();
if (q.empty()) {
m.unlock();
return false;
} else {
val = q.front();
q.erase(q.begin());
m.unlock();
return true;
}
}
// empty 返回队列是否为空
bool empty() {
m.lock();
bool isEmpty = q.empty();
m.unlock();
return isEmpty;
}
// size 返回队列的大小
int size() {
m.lock();
int s = q.size();
m.unlock();
return s;
}
protected:
std::vector<T> q; // 容器
std::mutex m; // 锁
};
それは二つのスレッドで演奏されているので、しかし、あなたは、現在再生中の映像や音声の情報環境を再生するときに知っておく必要があります。だから我々は、各スレッドのスレッドを作成するために、環境に関するいくつかの基本的な情報パッケージを送信します。
#define MAX_AUDIO_FRAME_SIZE 192000 //采样率:1 second of 48khz 32bit audio
class PlayerContext {
public:
PlayerContext();
AVFormatContext* pFormateCtx; // AV文件的上下文
std::atomic_bool quit; // 退出标志
// --------------------------- 音频相关参数 ---------------------------- //
AVCodecParameters* audioCodecParameter; // 音频解码器的参数
AVCodecContext* audioCodecCtx; // 音频解码器的上下文
AVCodec* audioCodec; // 音频解码器
AVStream* audio_stream; // 音频流
int au_stream_index; // 记录音频流的位置
double audio_clk; // 当前音频时间
int64_t audio_pts; // 记录当前已经播放音频的时间
int64_t audio_pts_duration; // 记录当前已经播放音频的时间
Uint8* audio_pos; // 用来控制每次
Uint32 audio_len; // 用来控制
Queue<AVPacket*> audio_queue; // 音频包队列
SwrContext* au_convert_ctx; // 音频转换器
AVSampleFormat out_sample_fmt; // 重采样格式,默认设置为 AV_SAMPLE_FMT_S16
int out_buffer_size; // 重采样后的buffer大小
uint8_t* out_buffer; // 重采样后的buffer
SDL_AudioSpec wanted_spec; // sdl系统播放音频的各项参数信息
// ------------------------------ end --------------------------------- //
// ------------------------ 视频相关参数 --------------------------- //
AVCodecParameters* videoCodecParameter; // 视频解码器的参数
AVCodecContext* videoCodecCtx; // 视频解码器的上下文
AVCodec* pVideoCodec; // 视频解码器
AVStream* video_stream; // 视频流
Queue<AVPacket*> video_queue; // 视频包队列
SwsContext* vi_convert_ctx; // 视频转换器
AVFrame* pFrameYUV; // 存放转换后的视频
int video_stream_index; // 记录视频流的位置
int64_t video_pts; // 记录当前已经播放了的视频时间
double video_clk; // 当前视频帧的时间戳
// ---------------------------- end ------------------------------ //
// ---------------------------- sdl ----------------------------- //
SDL_Window* screen; // 视频窗口
SDL_Renderer* renderer; // 渲染器
SDL_Texture* texture; // 纹理
SDL_Rect sdlRect;
// --------------------------- end ------------------------------ //
};
これらのパラメータは、ここに書き留めておきますが、まだ完全には理解されていません。以下の使用している場合は、自然に理解するだろう。スペースの制約のこれが立証されていないためにも、このクラスのコンストラクタでも、初期設定の値で作られ、それは、操作をゼロに実質的に空ポインタです。必要ではgithubの上のソースコードを表示するために行くことができます。
第二に、オーディオとビデオのパラメータの基本的な構成
同様に、私たちがしなければならない上記にファイルに保存されたオーディオおよびビデオ情報を取得し、オーディオとビデオデコーダを構成しています。
基本的なファイル情報を取得
以前のアプローチと完全に一致した方法でファイル情報を取得します。最後のコールinit_audio_parameters、init_video_paramertersパラメータ情報機能は、2つのオーディオおよびビデオを初期化します。情報はに格納されplayerCtxに。
// 注册所有编码器
av_register_all();
// 音视频的环境
PlayerContext playerCtx;
//读取文件头的格式信息储存到pFormateCtx中
if (avformat_open_input(&playerCtx.pFormateCtx, filePath, nullptr, 0) != 0) {
printf_s("avformat_open_input failed.\n");
return -1;
}
//读取文件中的流信息储存到pFormateCtx中
if (avformat_find_stream_info(playerCtx.pFormateCtx, nullptr) < 0) {
printf_s("avformat_find_stream_info failed.\n");
return -1;
}
// 将文件信息储存到标准错误上
av_dump_format(playerCtx.pFormateCtx, 0, filePath, 0);
// 查找音频流和视频流的位置
for (unsigned i = 0; i < playerCtx.pFormateCtx->nb_streams; ++i)
{
if (AVMEDIA_TYPE_VIDEO == playerCtx.pFormateCtx->streams[i]->codecpar->codec_type
&& playerCtx.video_stream_index < 0) { // 获取视频流位置
playerCtx.video_stream_index = i;
playerCtx.video_stream = playerCtx.pFormateCtx->streams[i];
}
if (AVMEDIA_TYPE_AUDIO == playerCtx.pFormateCtx->streams[i]->codecpar->codec_type
&& playerCtx.au_stream_index < 0) { // 获取音频流位置
playerCtx.au_stream_index = i;
playerCtx.audio_stream = playerCtx.pFormateCtx->streams[i];
continue;
}
}
// 异常处理
if (playerCtx.video_stream_index == -1)
return -1;
if (playerCtx.au_stream_index == -1)
return -1;
// 初始化 SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO))
{
printf("Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}
// 初始化音频参数
if (init_audio_parameters(playerCtx) < 0) {
return -1;
}
// 初始化视频参数
if (init_video_paramerters(playerCtx) < 0) {
return -1;
}
オーディオの初期化パラメータ
あなたは、同じ基本のように、ここでそれらを繰り返さないでください、記事のオーディオ部分について読むことができますinit_audio_parameters()ソースでの実装を見てください。ビデオの初期化については、次のハイライト。
ビデオパラメータを初期化します
初期のビデオパラメータは、2つの部分に分割します。ビデオデコーダの初期化のffmpegの部分。これは、オーディオとの部分に似ています。ビデオの別の部分は、ビデオインターフェースの設定パラメータをレンダリングするユニークです。そのコアの目的ではありませんビデオとオーディオのこの部分は、ビデオをレンダリングすることです。この部分は、GUIライブラリの多くを達成するために使用することができます。本実施形態では、映像をレンダリングするように、SDLと他のライブラリの導入を軽減するためです。
私は主に3つの構造を通じて、SDLによって映像を表示します:
- SDL_Window:ビデオウィンドウによる()SDL_CreateWindow作成しました。
- SDL_Texture:ビデオテクスチャ:によって()SDL_CreateTexture作成しました。SDLビデオテクスチャが表示される映像情報を格納するために使用されます。YUVフォーマットで、ここで使用される多くのビデオフォーマットがあります。
- SDL_Render:ビデオレンダラによるSDL_CreateRenderer()が作成されました。SDLレンダラは、ビデオウィンドウを表示するために使用されます。
あなたが最初の3つの構造を作成し、動画情報更新のテクスチャ(それぞれ取得し、映像を表示したいSDL_UpdateYUVTexture()およびリセット)
レンダラを(SDL_RenderClear() )。そして、レンダラに新しいテクスチャ情報をコピー(SDL_RenderCopy() )、そして最終的に映像表示レンダラによって
アップ(SDL_RenderPresent() )
// init_video_paramerters 初始化视频参数,sws转换器所需的各项参数
int init_video_paramerters(PlayerContext& playerCtx) {
// 获取视频解码器参数
playerCtx.videoCodecParameter = playerCtx.pFormateCtx->streams[playerCtx.video_stream_index]->codecpar;
// 获取视频解码器
playerCtx.pVideoCodec = avcodec_find_decoder(playerCtx.videoCodecParameter->codec_id);
if (nullptr == playerCtx.pVideoCodec) {
printf_s("video avcodec_find_decoder failed.\n");
return -1;
}
// 获取解码器上下文
playerCtx.videoCodecCtx = avcodec_alloc_context3(playerCtx.pVideoCodec);
// 根据视频参数配置视频编码器
if (avcodec_parameters_to_context(playerCtx.videoCodecCtx, playerCtx.videoCodecParameter) < 0) {
printf_s("video avcodec_parameters_to_context failed\n");
return -1;
}
// 根据上下文配置视频解码器
avcodec_open2(playerCtx.videoCodecCtx, playerCtx.pVideoCodec, nullptr);
// 创建一个SDL窗口 SDL2.0之后的版本
playerCtx.screen = SDL_CreateWindow("MediaPlayer",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
//playerCtx.videoCodecCtx->width, playerCtx.videoCodecCtx->height,
1280, 720,
SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL);
if (!playerCtx.screen) {
fprintf(stderr, "SDL: could not set video mode - exiting\n");
exit(1);
}
// 创建一个SDL渲染器
playerCtx.renderer = SDL_CreateRenderer(playerCtx.screen, -1, 0);
// 创建一个SDL纹理
playerCtx.texture = SDL_CreateTexture(playerCtx.renderer,
SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
playerCtx.videoCodecCtx->width, playerCtx.videoCodecCtx->height);
// 设置SDL渲染的区域
playerCtx.sdlRect.x = 0;
playerCtx.sdlRect.y = 0;
playerCtx.sdlRect.w = 1280;// playerCtx.videoCodecCtx->width;
playerCtx.sdlRect.h = 720;// playerCtx.videoCodecCtx->height;
// 设置视频缩放转换器
playerCtx.vi_convert_ctx = sws_getContext(playerCtx.videoCodecCtx->width, playerCtx.videoCodecCtx->height, playerCtx.videoCodecCtx->pix_fmt
, playerCtx.videoCodecCtx->width, playerCtx.videoCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, nullptr, nullptr, nullptr);
// 配置视频帧和视频像素空间
unsigned char* out_buffer = (unsigned char*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P
, playerCtx.videoCodecCtx->width, playerCtx.videoCodecCtx->height, 1));
playerCtx.pFrameYUV = av_frame_alloc(); // 配置视频帧
// 配置视频帧的像素数据空间
av_image_fill_arrays(playerCtx.pFrameYUV->data, playerCtx.pFrameYUV->linesize
, out_buffer, AV_PIX_FMT_YUV420P, playerCtx.videoCodecCtx->width, playerCtx.videoCodecCtx->height, 1);
return 0;
}
最後に役割をav_image_fill_arraysと、この関数の役割は、av_init_packetに似ています。AVFrameのデータにメモリ空間を作成するために使用されます。
第三に、ファイルから読み込んだオーディオとビデオパケット(AVPacket)
ビデオパケットを読み取り、音声がほとんど同じである方法をお読みください。
// 读取AVFrame并根据pkt的类型放入音频队列或视频队列中
AVPacket* packet = nullptr;
while (!playerCtx.quit) {
// 判断缓存是否填满,填满则等待消耗后再继续填缓存
if (playerCtx.audio_queue.size() > 50 ||
playerCtx.video_queue.size() > 100) {
SDL_Delay(10);
continue;
}
packet = av_packet_alloc();
av_init_packet(packet);
if (av_read_frame(playerCtx.pFormateCtx, packet) < 0) { // 从AV文件中读取Frame
break;
}
if (packet->stream_index == playerCtx.au_stream_index) { // 将音频帧存入到音频缓存队列中,在音频的解码线程中解码
playerCtx.audio_queue.push(packet);
}
else if (packet->stream_index == playerCtx.video_stream_index) { //将视频存入到视频缓存队列中,在视频的解码线程中解码
playerCtx.video_queue.push(packet);
}
else {
av_packet_unref(packet);
av_packet_free(&packet);
}
}
ここではそれらを繰り返さないオーディオパッケージを読み込む方法を以前に詳細に説明しました。ここで注意すべきことの一つは、私たちはもはや直接デコードしていないということですが、我々は2つのキューのストアオーディオとビデオを押しました。
ビデオのデコード、再生、オーディオとビデオの同期などを参照してくださいパートIIについて