FFmpeg+SDL+Qt 构建简单视频播放器

本文主要讲述如何利用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)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

猜你喜欢

转载自blog.csdn.net/yinshipin007/article/details/130645890