ffmpeg + SDL2 实现播放器

Posted 师范大学生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ffmpeg + SDL2 实现播放器相关的知识,希望对你有一定的参考价值。

使用的ffmpeg版本为5.0.1,SDL的版本为2.022。c++环境为vs2017。

先上最简易的整体代码,初步实现了SDL和ffmpeg的结合。

#include<iostream>
#include<string.h>
#include<SDL.h>
extern "C"

#include "libavformat/avformat.h" //头文件不仅要在项目中鼠标点击配置,在代码中也要引入
#include "include/libavformat/avformat.h"
#include "include/libswscale/swscale.h"
#include "include/libavdevice/avdevice.h"
#include"libavcodec/avcodec.h"

using namespace std;

int main(int argc, char *argv[])

	int ret = 1;
	const char *file = "ds.mov";

	// 初始化ffmpeg组件
	AVFormatContext *pFormatCtx = nullptr;   //视频文件上下文
	int videostream;   //视频流标识
	AVCodecParameters *pCodeParameters = nullptr; //解码器相关参数
	const AVCodec *pCodec = nullptr;  //解码器
	AVCodecContext *pCodecCtx = nullptr;  // 解码器上下文
	AVFrame *pFrame = nullptr; //解码后的帧
	AVPacket packet; // 解码前的帧

	//初始化SDL组件
	SDL_Rect rect;                              //渲染显示面积
	SDL_Window *window = NULL;                  // 窗口
	SDL_Renderer *renderer = NULL;              // 渲染
	SDL_Texture *texture = NULL;                // 纹理
	Uint32 pixformat;

	//视频分辨率
	int w_width = 640;  
	int w_height = 480;

	//SDL初始化 
	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) 
		cout << "can not initialize SDL" << endl;
		return ret;
	

	//FFMPEG 视频文件读取
	if (avformat_open_input(&pFormatCtx, file, nullptr, nullptr) != 0) 
		cout << "can not open the video file" << endl;
		goto __FAIL;
	

	//FFMPEG 寻找视频流
	videostream = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1,nullptr,0);
	if (videostream == -1) 
		cout << "can not open a video stream" << endl;
		goto __FAIL;
	

	//FFMPEG 寻找合适的解码器
	pCodeParameters = pFormatCtx->streams[videostream]->codecpar;
	pCodec = avcodec_find_decoder(pCodeParameters->codec_id);
	if (pCodec == nullptr) 
		cout << "can not find a codec" << endl;
		goto __FAIL;
	

	//FFMPEG 解码器信息配置
	pCodecCtx = avcodec_alloc_context3(pCodec);
	if (avcodec_parameters_to_context(pCodecCtx, pCodeParameters) != 0) 
		cout << "can not copy codec context" << endl;
		goto __FAIL;
	

	//FFMPEG 解码器启动
	if (avcodec_open2(pCodecCtx, pCodec, nullptr) < 0) 
		cout << " can not open the decoder" << endl;
		goto __FAIL;
	

	//FFMPEG 初始化解码的帧
	pFrame = av_frame_alloc();

	//SDL 获得显示的视频画面的长度与宽度
	w_width = pCodecCtx->width;
	w_height = pCodecCtx->height;

	//SDL 窗口初始化
	window = SDL_CreateWindow("MEDIA PLAYER",
		SDL_WINDOWPOS_UNDEFINED,
		SDL_WINDOWPOS_UNDEFINED,
		w_width, w_height,
		SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
	if (!window) 
		cout << "can not create window" << endl;
		goto __FAIL;
	

	//SDL 渲染器初始化
	renderer = SDL_CreateRenderer(window, -1, 0);
	if (!renderer) 
		cout << "can not create renderer!" << endl;
		goto __FAIL;
	

	//SDL 视频格式与纹理初始化
	pixformat = SDL_PIXELFORMAT_IYUV;
	texture = SDL_CreateTexture(renderer,
		pixformat,
		SDL_TEXTUREACCESS_STREAMING,
		w_width,
		w_height);

	//主循环
	while (av_read_frame(pFormatCtx,&packet)>= 0) //FFMPEG 如果已经读到了一个帧
		if (packet.stream_index == videostream) //并且该帧是视频帧
			avcodec_send_packet(pCodecCtx,&packet);
			while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) 
				//SDL 刷新纹理
				SDL_UpdateYUVTexture(texture, NULL,
					pFrame->data[0], pFrame->linesize[0],
					pFrame->data[1], pFrame->linesize[1],
					pFrame->data[2], pFrame->linesize[2]);
				rect.x = 0;//SDL设置渲染目标的显示区域
				rect.y = 0;
				rect.w = pCodecCtx->width;
				rect.h = pCodecCtx->height;

				SDL_RenderClear(renderer);//SDL 清空渲染器内容
				SDL_RenderCopy(renderer, texture, NULL, &rect);//SDL 将纹理复制到渲染器
				SDL_RenderPresent(renderer);//SDL 渲染
			
		
		av_packet_unref(&packet);

		SDL_Event event;// SDL事件
		SDL_PollEvent(&event);// 轮询事件
		switch (event.type)
		
		case SDL_QUIT: //如果窗口被关闭
			goto __QUIT;
		default:
			break;
		
	
	

__QUIT:
	ret = 0;
__FAIL:
	if (pFrame) 
		av_frame_free(&pFrame);
		pFrame = nullptr;
	
	if (pCodecCtx) 
		avcodec_close(pCodecCtx);
		pCodecCtx = nullptr;
		pCodec = nullptr;
	
	if (pCodeParameters) 
		avcodec_parameters_free(&pCodeParameters);
		pCodeParameters = nullptr;
	
	/*if (pFormatCtx) 
		avformat_close_input(&pFormatCtx);
	*/

	if (pFormatCtx) 
		avformat_free_context(pFormatCtx);
		pFormatCtx = nullptr;
	
	
	if (texture) 
		SDL_DestroyTexture(texture);
		texture = nullptr;
	
	if (renderer) 
		SDL_DestroyRenderer(renderer);
		renderer = nullptr;
	
	if (window) 
		SDL_DestroyWindow(window);
		window = nullptr;
	

	SDL_Quit();
	cout << "succeed!" << endl;
	return ret;

以上代码在关键函数的注释中都已经注明了其属于哪个模块,有助于拆解学习。

以上代码的大部分内容在之前的文章都有所介绍,这里主要讲解新出现的知识。

av_read_frame(pFormatCtx,&packet)

在对视频的帧进行解码时,首先我们需要将帧从视频文件中提取出来,注意当前的帧是未解压缩的,属于packet。复习一下,packet是未解码的帧,frame是解码完成了的帧。

av_read_frame()实现的正是此功能,该函数确切的说是从视频文件中读取一个packet出来。并且该函数可以保证读取的帧是完整的。

而在之前的文章中提到,一个packet只含有一个视频帧,但是可以包含多个音频帧。

对于音频,如果每个帧具有已知的固定大小(例如PCM或ADPCM数据),则它包含整数帧数。
如果每个帧的大小可变(MPEG),则包含一个帧。

通过将此函数作为主循环的工作条件,可以保证在从视频文件中读到多媒体帧后就工作,读不到多媒体帧就不工作。

avcodec_send_packet(pCodecCtx,&packet)

该函数正是ffmpeg对视频进行解码操作的函数,该函数的工作内容和名字一样,将packet数据传入到codec进行解码,得到解码后的frame数据。

avcodec_receive_frame(pCodecCtx, pFrame)

该函数是与解码操作配合的函数,在codec解码器将packet解码为frame后使用该函数将frame读取出来,frame变量就是当前的解码的帧。

这里注意一下,avcodec_receive_frame(pCodecCtx, pFrame)与avcodec_send_packet(pCodecCtx,&packet)这两个函数是一一对应的么?其实应该不是,因为上文提过,一个packet可能解出多个frame出来(在音频帧的情况下),因此send函数应该是对应多个receive函数的。

当我们不知道一个函数会执行多少次时,我们可以将此函数放入while()循环的判断语句中,当函数的返回值发生变化,说明该函数不需要再被执行,循环内的相关处理语句也不会再执行。

SDL与ffmpeg的结合点

通过之前文章可以知道,SDL是通过将视频帧“刷”到纹理上,之后实现渲染播放的,我们将ffmpeg解码后的frame刷到纹理上,就实现了两者的结合。

SDL_UpdateYUVTexture(texture, NULL,
					pFrame->data[0], pFrame->linesize[0],
					pFrame->data[1], pFrame->linesize[1],
					pFrame->data[2], pFrame->linesize[2]);

刷新纹理的函数中,分别对y、u、v三个通道的数据进行了加载,等于是刷进了纹理,之后的操作就和 SDL播放视频没有什么区别了。

FFMPEG+SDL实现视频播放器

一. 前言

基于学习ffmpeg和sdl,写一个视频播放器是个不错的练手项目。
视频播放器的原理很多人的博客都有讲过,这里出于自己总结的目的,还是会做一些概况。

二. 视频播放器基本原理

技术图片

2.1 解封装

视频文件基本上都是将编码好的音频和视频数据封装在一起形成的,因此拿到视频文件的第一步就是先将它解封装,分为视频流和音频流压缩编码数据。常见的封装格式有MP4、MKV、FLV、AVI、RMVB、TS等。例如,FLV格式的文件经过解封装后,可能得到H.264编码的视频码流和AAC编码的音频码流。
在FFMPEG中,解封装的流程如下:
技术图片
这一步最重要的是得到解封装器的上下文结构体"AVFormatContext *m_pFormatCtx", 以及接下来我们要解码的音视频流索引。

2.2 解码

原始数据基本上都是经过压缩编码后的数据,解码过程就是将H.264、AAC等压缩后的数据解码成非压缩的音频/视频原始数据,视频一般是YUV或者RGB数据,音频一般是PCM抽样数据。
解码过程可以总结如下:
技术图片

2.3 SDL2播放视频数据

我们都知道视频其实都是由连续的一帧帧图像快速播放形成的动态效果,一般视频都设置成了25帧,即1s内播放25幅图片。
我们使用SDL2库来播放视频。这和我之前的SDL2学习(一): 显示一张图片中写到的SDL2显示一张图片就关联了起来,不过这里更加复杂点。
在视频解码完后,我们在avcodec_receive_frame得到的AVFrame对象,就是视频的一帧数据。我们要做的是将这一帧的数据显示到SDL的Render中。总体流程如下:
技术图片
首先我们需要对得到的AVFrame数据进行大小格式的变换,这里使用sws_scale函数实现,之后就是更新SDL中的Texture和Render了。下面是关键代码:

AVFrame *frame = m_videoFrameQueue.front();
m_videoFrameQueue.pop();

AVFrame *frameYUV = av_frame_alloc();
int ret = av_image_alloc(frameYUV->data, frameYUV->linesize, m_sdlRect.w, m_sdlRect.h, AV_PIX_FMT_YUV420P, 1);
//Convert image
if (m_imgConvertCtx)
{
sws_scale(m_imgConvertCtx, frame->data, frame->linesize, 0, m_videoCodecParams.height, frameYUV->data, frameYUV->linesize);
SDL_UpdateYUVTexture(m_sdlTexture, NULL, frameYUV->data[0], frameYUV->linesize[0], frameYUV->data[1], frameYUV->linesize[1], frameYUV->data[2], frameYUV->linesize[2]);
SDL_RenderClear(m_sdlRender);
SDL_RenderCopy(m_sdlRender, m_sdlTexture, NULL, &m_sdlRect);

// Present picture
SDL_RenderPresent(m_sdlRender);
}

2.4?SDL2播放音频数据

对于音频数据,avcodec_receive_frame后得到的AVFrame是音频的pcm数据,但是它不向视频那样表示"一帧",它可能包含很多的sample,即多次的采样数据。
播放音频,同样需要对音频数据进行格式转换,以支持音频设备的播放。音频格式转换主要通过swr_convert函数完成。转换后的音频数据可以放到一个公共缓冲区中。
播放音频使用SDL_OpenAudio函数,它需要闯入一个SDL_AudioSpec结构体用于设置播放参数,其中需要设置一个callback用于音频设备取数据时执行,因此我们需要在这个回调里向音频设备"喂"数据:

SDL_AudioSpec m_sdlAudioSpec;
auto audioCtx = m_audioDecoder.GetCodecContext();

m_sdlAudioSpec.freq = audioCtx->sample_rate; //根据你录制的PCM采样率决定
m_sdlAudioSpec.format = AUDIO_S16SYS;
m_sdlAudioSpec.channels = audioCtx->channels;
m_sdlAudioSpec.silence = 0;
m_sdlAudioSpec.samples = SDL_AUDIO_BUFFER_SIZE;
m_sdlAudioSpec.callback = &SDLVideoPlayer::ReadAudioData;
m_sdlAudioSpec.userdata = NULL;

int re = SDL_OpenAudio(&m_sdlAudioSpec, NULL);
if (re < 0)
{
    std::cout << "can't open audio: " << GetErrorInfo(re);
}
else
{
    //Start play audio
    SDL_PauseAudio(0);
}

void SDLVideoPlayer::ReadAudioData(void *udata, Uint8 *stream, int len) {
    SDL_memset(stream, 0, len);
    //需要向stream中填充len长度的音频数据
    ...
    SDL_MixAudio(stream, m_audioPcmDataBuf, len, g_volum);
}

2.5 音视频同步的设计

用两个线程分别播放音频和视频,音频的话可以直接在所设置的回调中喂数据即可,而视频则需要我们自己来控制播放速度,这就涉及到两者播放速度的统一问题。
音视频同步的基本方式就是确定一个时钟作为主时钟,播放过程中,主时钟作为同步基准,不断判断当前流的播放时间和主时钟的差异,以调节自身的播放速度。按照主时钟的不同种类,可以分为:

  • 音频同步到视频,视频时钟作为主时钟;
  • 视频同步到音频,音频时钟作为主时钟;
  • 音视频都同步到外部时钟。

由于音频播放时往往都是送很多数据到设备缓存中,而且音频播放效果对人的敏感度更高,因此以音频时钟为主是比较合理且简单的办法。具体实现就是:

  1. 在每次喂音频数据的时候,记录送入数据的起始pts时间戳,表示当前音频的播放进度;
  2. 每次刷新图片时,记录当前图片帧的pts时间戳;
  3. 在记录当前音频pts的同时,根据记录的图片pts,记录两者间的延时delay;
  4. 刷新图片时,根据delay值判断,当前视频如果比音频快,那么一次性调整视频等待时间为正常两帧间隔加音视频之间的延时,之后将delay置0;如果音频比视频快,那么直接丢弃当前的视频帧,直到和音频时间一致。

2.6 快进和快退

快进和快退,或者一些播放器直接拖动进度条,实现思路都是一样的,即使用av_seek_frame实现:

 av_seek_frame(m_pFormatCtx, -1, pts * AV_TIME_BASE, AVSEEK_FLAG_BACKWARD);

因此关键就是获取要跳转的时间戳,这个在做音视频同步处理后,这个时间戳就很容易拿到。

2.7 SDL事件处理

对于窗口大小更改、暂停、快进快退等,都是需要交互的,这个可以通过SDL的事件机制来实现。
监听事件:

SDL_Event event;
SDL_WaitEvent(&event);
if (event.type == SDL_WINDOWEVENT) {
    ...
}
...

除了预定义的事件,比如窗口事件、鼠标事件、按键事件等,你也可以自己触发或定义新的事件:

SDL_Event event;
event.type = SFM_REFRESH_PIC_EVENT;
SDL_PushEvent(&event);

我这里就是使用SDL事件来通知视频播放线程来进行下一帧的播放。

以上是关于ffmpeg + SDL2 实现播放器的主要内容,如果未能解决你的问题,请参考以下文章

[SimplePlayer] 实现一个简单的播放器

最简单的基于FFMPEG+SDL的视频播放器 ver2 (採用SDL2.0)

SDL2播放FFmpeg解压的视频

ffmpeg解码之使用C语言打印音视频信息

基于FFmpeg的视频播放器之五:使用SDL2渲染yuv420p

如何在Android用FFmpeg+SDL2.0解码显示图像