【项目教程】FFmpeg+SDL2实现视频播放器

devtools/2024/11/15 0:59:21/

一、前言

学习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帧图像。

在播放视频时,我们使用SDL2库。每个图像在SDL2中被表示为一个纹理,而纹理与SDL2的渲染器相关联。

在视频解码后,我们可以从avcodec_receive_frame函数中获取到一个AVFrame对象,该对象包含了一帧视频数据。我们的目标是将这一帧的数据渲染到SDL的渲染器中。总体流程如下所示:

首先,我们需要使用sws_scale函数对获取到的AVFrame数据进行大小和格式的转换。接下来,我们需要更新SDL2中的纹理(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数据。

与视频数据不同,音频数据并不是以帧为单位表示的,它可能包含多个采样数据(samples)。 为了播放音频,我们同样需要对音频数据进行格式转换,以适应音频设备的播放要求。音频格式转换主要通过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 audioSDL_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. 音频同步到视频:使用视频时钟作为主时钟。
  2. 视频同步到音频:使用音频时钟作为主时钟。
  3. 音视频都同步到外部时钟。 由于音频播放通常会将大量数据发送到设备缓存中,并且音频对人的敏感度更高,因此以音频时钟作为主时钟是比较合理且简单的方法。具体实现如下:
  4. 在每次传递音频数据时,记录送入数据的起始pts时间戳,表示当前音频的播放进度。
  5. 每次刷新视频帧时,记录当前图片帧的pts时间戳。
  6. 在记录当前音频pts的同时,根据记录的图片pts,计算两者之间的延迟。
  7. 在刷新视频帧时,根据延迟值判断,如果当前视频比音频快,那么调整视频等待时间为正常两帧之间的间隔加上音视频之间的延迟,并将延迟值置为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);


http://www.ppmy.cn/devtools/43212.html

相关文章

Jenkins - Pipeline Retry

Jenkins - Pipeline Retry 引言retryretry 实例 引言 日常运行自动化测试用例&#xff0c;通常是晚上定时启动 pipeline job&#xff0c;一个 pipeline 脚本可能会涉及到多个 Job, 最后 post 发邮件汇总测试 report。有时会遇到 Jenkins 环境问题导致某 Job 失败&#xff0c;第…

零基础小白本地部署大疆上云api(个人记录供参考)

文章目录 运行前准备前后端项目运行1.前端项目&#xff1a; 后端项目运行必须先依靠emqx运行必须先依靠redis运行修改后端项目的application.yml文件 运行前准备 1.保证电脑又node.js环境&#xff0c;可以正常使用npm 2.Java的jdk必须是11及以上版本否则无效 3.下载好emqx,red…

c语言之文件格式化写入

向文件写入特定的信息&#xff0c;在c语言中需要fprintf语句 fprintf语句的格式是 fprintf(文件指针&#xff0c;格式字符串&#xff0c;输出表列&#xff09; 示例代码如下 #include<stdio.h>int main() {FILE *fp;int a123;float b3.1415f;fpfopen("eee.txt&q…

展现金融科技前沿力量,ATFX于哥伦比亚金融博览会绽放光彩

不到半个月的时间里&#xff0c;高光时刻再度降临ATFX。而这一次&#xff0c;是ATFX不曾拥有的桂冠—“全球最佳在线经纪商”(Best Global Online Broker)。2024年5月15日至16日&#xff0c;拉丁美洲首屈一指的金融盛会—2024年哥伦比亚金融博览会(Money Expo Colombia 2024) 于…

C++:vector基础讲解

hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起学习《C&#xff1a;vector基础讲解》&#xff0c;感谢大家对我上一篇的支持&#xff0c;如有什么问题&#xff0c;还请多多指教 &#xff01; 如果本篇文章对你有帮助&#xff0c;还请各位点点赞&#xff01;&#…

听说部门来了个00后测试开发,一顿操作给我整麻了

公司新来了个同事&#xff0c;听说大学是学的广告专业&#xff0c;因为喜欢IT行业就找了个培训班&#xff0c;后来在一家小公司实习半年&#xff0c;现在跳槽来我们公司。来了之后把现有项目的性能优化了一遍&#xff0c;服务器缩减一半&#xff0c;性能反而提升4倍&#xff01…

js实现鼠标拖拽多选功能

实现功能 在PC端的H5页面中&#xff0c;客户拖动鼠标可以连选多个选项 效果展示 具体代码如下 <!DOCTYPE html> <html><head><title>鼠标拖拽多选功能</title><script src"https://cdn.bootcss.com/jquery/1.10.2/jquery.min.js&quo…

CSS中的border-radius: 打造圆润世界的秘密武器

在Web设计的世界里&#xff0c;边角不再是直来直去的专利&#xff0c;CSS的border-radius属性赋予了我们创造圆润、柔和界面的能力。本文将深入探索border-radius的魅力&#xff0c;从基础用法到高级技巧&#xff0c;让你的网页元素更加生动和现代化。 一、border-radius初体验…