本文主要讲述如何利用FFmpeg,SDL,和 Qt构建一个简单的视频播放器。
FFmpeg是一个音视频处理的开源库,提供了C接口用于音视频的编解码、封装、流处理。在本教程中主要利用FFmpeg对视频封装文件进行解封装,解码。
SDL是音视频播放和渲染的一个开源库,主要利用它进行视频渲染和音频播放。
Qt主要用于写播放器简单UI,以及播放暂停音视频选择按钮。
首先要了解音视频的一些基本知识,平常所说的MP4,mkv文件是一个音视频封装文件,里面一般包含音频视频两条流,每条流存储着编码信息以及展示时间基等信息。
播放器的整个流程如下图,首先从服务器拉流,或者从本地打开视频文件(用FFmpeg处理时接口都一样,只是提供的地址不一样)。打开之后进行解封装,每次读取一个packet,是音频则解码成音频帧,视频则解码成视频帧。再对音视频帧做一个转换,转换成可以播放和渲染的格式,进行播放。
程序的整个流程如下图。首先需要写好Qt UI,对FFmpeg进行初始化,对SDL进行初始化,然后打开输入源,同时打开相应的解码器,设置播放格式,根据输入源格式和播放格式创建转换器。再创建音视频播放线程,接着就可以开始读文件了,每次读到一个packet就根据stream_index判断是否为音视频,是音频则放入音频解码器解码成音频帧,再转换格式,送入音频缓存中,是视频则放入视频解码器并转换。再继续读下一packet.音视频播放则由不同的线程完成,所以还需要时间同步。一般而言以音频为准,视频根据音频的播放时间进行渲染。
所以以下就分五个部分讲解整个流程,分别是初始化,解封装,解码,转换,播放。整个工程用Qt作为框架,需要创建qt app。
初始化
FFmpeg网络初始化,因为我们的程序是从rtmp服务器拉流,所以需要对网络做一个初始化。
avformat_network_init();
SDL初始化,初始化音频和视频模块以及时间模块。
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
TestNotNull(NULL, SDL_GetError());
}
以及画好界面窗口和按钮之后将其组织起来并设置槽函数。
ui.audio->setCheckState(Qt::Checked);
ui.video->setCheckState(Qt::Checked);
//水平布局,控制按钮
QBoxLayout *ctlLayout = new QHBoxLayout;
ctlLayout->addWidget(ui.pauseBtn, 5, Qt::AlignCenter);
ctlLayout->addWidget(ui.video);
ctlLayout->addWidget(ui.audio);
//垂直布局:视频播放器、进度条、控制按钮布局
QBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(videoWidget);
mainLayout->addLayout(ctlLayout);
//设置布局
mainWindowWidget->setLayout(mainLayout);
//设置槽函数
connect(ui.pauseBtn, SIGNAL(clicked()), this, SLOT(ChangePlay()));
connect(ui.audio, SIGNAL(stateChanged(int)), this, SLOT(ChangeAudio()));
connect(ui.video, SIGNAL(stateChanged(int)), this, SLOT(ChangeVideo()));
解封装
解封装之前需要打开输入,并找到相应的音视频流的index,用于以后判断从文件读取的packet属于哪个流。
//打开输入
if (avformat_open_input(&pFmtCtx, fileName.c_str(), NULL, NULL) < 0) {
TestNotNull(NULL, "Failed to open input file: " + fileName);
}
//找编码信息
if (avformat_find_stream_info(pFmtCtx, NULL) < 0) {
TestNotNull(NULL, "Failed to find stream information.");
}
//设置音视频的 index
for (int i = 0; i < pFmtCtx->nb_streams; i++) {
if (pFmtCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO)
aIndex = i;
}//for
//设置视频索引
for (int i = 0; i < pFmtCtx->nb_streams; i++) {
if (pFmtCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
vIndex = i;
}
打开音视频解码器,一般来讲是重新申请一个解码器CodecContext,但是官网给的很多例子也是直接指向stream里面的解码器并将其打开,所以我也这么做了。
//打开音频解码器
pAudioCdcCtx = pFmtCtx->streams[aIndex]->codec;
AVCodec* pAudioCdc = avcodec_find_decoder(pAudioCdcCtx->codec_id);
TestNotNull(pAudioCdc, "Failed to find audio decoder.");
if (avcodec_open2(pAudioCdcCtx, pAudioCdc, NULL) < 0) {
TestNotNull(NULL, "Failed to open audio codec.");
}
//打开视频解码器
pVideoCdcCtx = pFmtCtx->streams[vIndex]->codec;
AVCodec* pVideoCdc = avcodec_find_decoder(pVideoCdcCtx->codec_id);
TestNotNull(pVideoCdc, "Failed to find decoder.");
if (avcodec_open2(pVideoCdcCtx, pVideoCdc, NULL) < 0) {
TestNotNull(NULL, "Failed to open codec.");
}
然后调用av_read_frame()函数读取文件的一个packet.
AVPacket pkt = { 0 };
av_init_packet(&pkt);
av_read_frame(pFmtCtx, &pkt);
转换
视频帧主要是转换一帧视频像素的编码格式以及长度和宽度。
//设置
pSwsCtx = sws_getContext(pIn->width,
pIn->height,
(AVPixelFormat)pOut->format,
pOut->width,
pOut->height,
(AVPixelFormat)pOut->format,
SWS_BILINEAR, NULL, NULL, NULL);
....
//转换
sws_scale(pSwsCtx, (uint8_t const* const*)pInFrm->data,
pInFrm->linesize, 0, VIDEO_HEIGH, pFrmYUV->data, pFrmYUV->linesize);
音频帧主要转换音频的采集格式声道信息以及采样率。
//转换设置
pSwrCtx = swr_alloc_set_opts(NULL,
pOutPara->channel_layout,
(AVSampleFormat)pOutPara->format,
pOutPara->sample_rate,
pInPara->channel_layout,
(AVSampleFormat)pInPara->format,
pInPara->sample_rate,
0, NULL);
.....
//转换
swr_convert(pSwrCtx, &(pBuff->dataPos), pBuff->dataLen,
(const uint8_t**)pFrm->data, pFrm->nb_samples)
播放
音频播放比较复杂。首先需要设置一个SDL_AudioSpec结构体,里面会传入一个回调函数,在用SDL_OpenAudio()函数打开播放器时,当音频播放完数据时就会去调用这个回调函数,我的回调函数如下:
void CallBackFunc(void * userdata, Uint8 * stream, int len)
{
SDL_memset(stream, 0, len);
PlayBuffer* playData = (PlayBuffer*)userdata;
if (playData->dataLen == 0)
return;
/* Mix as much data as possible */
len = (len > playData->dataLen ? playData->dataLen : len);
SDL_MixAudio(stream, playData->dataPos, len, SDL_MIX_MAXVOLUME);
playData->dataPos += len;
playData->dataLen -= len;
}
此外我还设置了一个线程,一个不断从音频缓存队列读取一段数据,并改变userdata参数,使得userdata 的指针不断更新,(实际上这个线程的工作可以放到回调函数去做,让回调函数自己从缓存队列里面读取数据)。这个线程代码如下:
void Player::PlayAudio()
{
while (true)
{
if (ctrl.GetPlayStatus() && ctrl.GetAudioStatus()) {
SDL_PauseAudio(0);
aPlay.WaitToPlay();
}
else {
SDL_PauseAudio(1);
}
}
}
视频播放相对来说逻辑比较好理解,首先设置窗口,不断地从缓存队列里面读取一帧,根据音频播放时间来决定是否播放。
设置窗口代码如下,需要说明的时创建窗口和渲染需要在同一个线程,否则渲染时会出现Reset() INVALIDCALL的错误。winid是qt窗口的id,这样SDL窗口就会嵌入到Qt窗口里面。
int VideoPlay::SDLInit(int w, int h, void* winid)
{
//设置渲染长方形
pScreen = SDL_CreateWindowFrom(winid);
TestNotNull(pScreen, SDL_GetError());
//创建窗口
pRender = SDL_CreateRenderer(pScreen, -1,
SDL_RENDERER_ACCELERATED);
TestNotNull(pRender, SDL_GetError());
//SDL_HINT_VIDEO_WINDOW_SHARE_PIXEL_FORMAT
pTexture = SDL_CreateTexture(pRender,
SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
rect.w, rect.h);
TestNotNull(pTexture, SDL_GetError());
return 0;
}
视频播放线程函数:
void Player::PlayVideo(void* p)
{
vPlay.SDLInit(0,0,p);
while (true) {
//获取音频播放时间
int64_t ts = aPlay.GetCurrentTime();
if (ctrl.GetPlayStatus() && ctrl.GetVideoStatus()) {
//控制器允许播放
vPlay.Render(ts);
}
else {
//显示黑屏
vPlay.BlackScreen();
}
}
}
渲染函数:
int VideoPlay::Render(int64_t ts) {
//从队列中获取一帧
if (pFrame == NULL) {
pFrame = (AVFrame*)cache->ComsumeElem();
}
int64_t diff = pFrame->pts/VIDEO_TIME_BASE - ts/AUDIO_TIME_BASE;
if (diff< -25 || diff > 1000){
//晚了25毫秒以上,或者早于100毫秒丢弃
printf("drop frame, diff = %d\n", diff);
av_frame_free(&pFrame);
pFrame = NULL;
return 0;
}
if (diff > 20)
{
Uint32 delay = diff - 20;
//printf("没到时间, sleep %d ms\n", delay);
SDL_Delay(1);
return 0;
}
//刷新渲染层
int ret = SDL_UpdateYUVTexture(pTexture, &rect,
pFrame->data[0], pFrame->linesize[0],
pFrame->data[1], pFrame->linesize[1],
pFrame->data[2], pFrame->linesize[2]);
if (ret < 0) {
TestNotNull(NULL, "texture is not valid.");
}
//清除、复制、播放
//if (SDL_RenderClear(pRender) < 0) {
// TestNotNull(NULL, SDL_GetError());
//}
if (SDL_RenderCopy(pRender, pTexture, &rect, NULL) < 0) {
TestNotNull(NULL, SDL_GetError());
}
SDL_RenderPresent(pRender);
printf("play video.\n");
av_frame_free(&pFrame);
pFrame = NULL;
return 0;
}
以上就是主要流程,效果如图:
原文 FFmpeg+SDL+Qt 构建简单视频播放器 - 知乎
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓