FFmpeg简易播放器的实现2-视频播放
本文为作者原创:https://www.cnblogs.com/leisure_chn/p/10047035.html,转载请注明出处
基于 FFmpeg 和 SDL 实现的简易视频播放器,主要分为读取视频文件解码和调用 SDL 播放两大部分。本实验仅研究视频播放的实现方式。
FFmpeg 简易播放器系列文章如下:
[1]. FFmpeg简易播放器的实现1-最简版
[2]. FFmpeg简易播放器的实现2-视频播放
[3]. FFmpeg简易播放器的实现3-音频播放
[4]. FFmpeg简易播放器的实现4-音视频播放
[5]. FFmpeg简易播放器的实现5-音视频同步
1. 视频播放器基本原理
下图引用自 “雷霄骅,视音频编解码技术零基础学习方法”,因原图太小,看不太清楚,故重新制作了一张图片。
如下内容引用自 “雷霄骅,视音频编解码技术零基础学习方法”:
解协议
将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如 HTTP,RTMP,或是 MMS 等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用 RTMP 协议传输的数据,经过解协议操作后,输出 FLV 格式的数据。解封装
将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如 MP4,MKV,RMVB,TS,FLV,AVI 等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV 格式的数据,经过解封装操作后,输出 H.264 编码的视频码流和 AAC 编码的音频码流。解码
将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含 AAC,MP3,AC-3 等等,视频的压缩编码标准则包含 H.264,MPEG2,VC-1 等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如 YUV420P,RGB 等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如 PCM 数据。音视频同步
根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。
2. 简易播放器的实现-视频播放
2.1 实验平台
实验平台: openSUSE Leap 15.6
FFmpeg版本:7.1.1
SDL版本: 2.0
FFmpeg 开发环境搭建可参考 “FFmpeg开发环境构建”
2.2 源码清单
下载源码:
git clone https://github.com/leichn/ffmpeg_training.git
ffmpeg_training/player_video
目录是本文源码目录。ffmpeg_training/video
目录存放了一些视频文件可用于测试。
2.3 源码流程简述
流程比较简单,不画流程图了,简述如下:
media file --[decode]--> raw frame --[scale]--> yuv frame --[SDL]--> display
media file ------------> p_frm_raw -----------> p_frm_yuv ---------> p_sdl_renderer
加上相关关键函数后,流程如下:
media_file ---[av_read_frame()]----------->
p_packet ---[avcodec_send_packet()]----->
decoder ---[avcodec_receive_frame()]--->
p_frm_raw ---[sws_scale()]--------------->
p_frm_yuv ---[SDL_UpdateYUVTexture()]---->
display
2.4 解码及显示过程
2.4.1 读取视频数据
调用 av_read_frame() 从输入文件中读取视频数据包:
// 从视频文件中读取一个 packet
// packet 可能是视频帧、音频帧或其他数据,解码器只会解码视频帧或音频帧
// 对于视频来说,一个 packet 只包含一个 frame
// 对于音频来说,若是帧长固定的格式则一个 packet 可包含多个 frame,
// 若是帧长可变的格式则一个 packet 只包含一个 frame
while (av_read_frame(p_fmt_ctx, p_packet) == 0)
{
// 取到一帧视频帧,则 break 退出。本实验不考虑音频,音频帧直接扔掉
if (p_packet->stream_index == video_idx)
{
break;
}
}
2.4.2 视频数据解码
调用 avcodec_send_packet() 和 avcodec_receive_frame() 对视频数据解码:
// 向解码器喂数据,一个 packet 就是一个压缩视频帧
// 注意:一般文件读完后(遇到 EOF),要向解码器发送一个空 packet 作为 flush packet,
// 发送第一个 flush packet 会返回成功,之后再发送 flush packet 则会返回 AVERROR_EOF
ret = avcodec_send_packet(p_codec_ctx, p_packet);
if (ret != 0)
{
printf("avcodec_send_packet() failed %d\n", ret);
res = -1;
goto exit8;
}
// 接收解码器输出的数据,得到一个原始视频帧,存在 p_frm_raw 中
ret = avcodec_receive_frame(p_codec_ctx, p_frm_raw);
if (ret != 0)
{
if (ret == AVERROR_EOF)
{
printf("avcodec_receive_frame(): the decoder has been fully flushed\n");
}
else if (ret == AVERROR(EAGAIN))
{
printf("avcodec_receive_frame(): output is not available in this state - "
"user must try to send new input\n");
continue;
}
else if (ret == AVERROR(EINVAL))
{
printf("avcodec_receive_frame(): codec not opened, or it is an encoder\n");
}
else
{
printf("avcodec_receive_frame(): legitimate decoding errors\n");
}
res = -1;
goto exit8;
}
2.4.3 图像格式转换
图像格式转换的目的,是为了解码后的视频帧能被 SDL 正常显示。因为 FFmpeg 解码后得到的图像格式不一定就能被 SDL 支持,这种情况下不作图像转换是无法正常显示的。
图像转换初始化相关:
// 初始化 SWS context,用于后续图像转换
// 此处第 6 个参数使用的是 FFmpeg 中的像素格式,对比参考注释 B4
// 如果解码后得到图像格式不被 SDL 支持,不进行格式转换的话,SDL 是无法正常显示图像的
// 如果解码后得到图像格式能被 SDL 支持,则不必进行格式转换
// 这里为了编写代码简便,不管解码后输出帧是什么格式,统一转换为 AV_PIX_FMT_YUV420P 格式,
// FFmpeg 中的 AV_PIX_FMT_YUV420P 格式实际就是 SDL 中的 SDL_PIXELFORMAT_IYUV 格式
struct SwsContext* p_sws_ctx = sws_getContext(p_codec_ctx->width, // src width
p_codec_ctx->height, // src height
p_codec_ctx->pix_fmt, // src format
p_codec_ctx->width, // dst width
p_codec_ctx->height, // dst height
AV_PIX_FMT_YUV420P, // dst format
SWS_BICUBIC, // flags
NULL, // src filter
NULL, // dst filter
NULL // param
);
// 创建 SDL_Texture
// 一个 SDL_Texture 对应一帧 YUV 数据,同 SDL 1.x 中的 SDL_Overlay
// 此处第 2 个参数使用的是 SDL 中的像素格式,对比参考注释 A7
// SDL 中的 SDL_PIXELFORMAT_IYUV 格式实际就是FFmpeg 中的 AV_PIX_FMT_YUV420P 格式
SDL_Texture* p_sdl_texture = SDL_CreateTexture(p_sdl_renderer,
SDL_PIXELFORMAT_IYUV,
SDL_TEXTUREACCESS_STREAMING,
p_codec_ctx->width,
p_codec_ctx->height
);
图像格式转换过程调用 sws_scale() 实现:
// 图像转换:p_frm_raw->data ==> p_frm_yuv->data
// 将源图像中一片连续的区域经过处理后更新到目标图像对应区域,处理的图像区域必须逐行连续
// plane: 如 YUV420P 像素格式有 Y、U、V 三个 plane,NV12 像素格式有 Y 和 UV 两个 plane
// slice: 图像中一片连续的行,必须是连续的,顺序由顶部到底部或由底部到顶部
// stride/pitch: 一行图像所占的字节数,Stride=BytesPerPixel*Width+Padding,注意对齐
// AVFrame.*data[]: 每个数组元素指向对应 plane
// AVFrame.linesize[]: 每个数组元素表示对应 plane 中一行图像所占的字节数
sws_scale(p_sws_ctx, // sws context
(const uint8_t *const *)p_frm_raw->data, // src slice
p_frm_raw->linesize, // src stride
0, // src slice y
p_codec_ctx->height, // src slice height
p_frm_yuv->data, // dst planes
p_frm_yuv->linesize // dst strides
);
2.4.4 显示
调用 SDL 相关函数将图像在屏幕上显示:
// 使用新的 YUV 像素数据更新 SDL_Rect
SDL_UpdateYUVTexture(p_sdl_texture, // sdl texture
&sdl_rect, // sdl rect
p_frm_yuv->data[0], // y plane
p_frm_yuv->linesize[0], // y pitch
p_frm_yuv->data[1], // u plane
p_frm_yuv->linesize[1], // u pitch
p_frm_yuv->data[2], // v plane
p_frm_yuv->linesize[2] // v pitch
);
// 使用特定颜色清空当前渲染目标
SDL_RenderClear(p_sdl_renderer);
// 使用图像数据(texture)更新当前渲染目标
SDL_RenderCopy(p_sdl_renderer, // sdl renderer
p_sdl_texture, // sdl texture
NULL, // src rect, if NULL copy texture
&sdl_rect // dst rect
);
// 执行渲染,更新屏幕显示
SDL_RenderPresent(p_sdl_renderer);
2.5 帧率控制-定时刷新机制
上一版源码只有一个线程,帧率控制过简单延时实现,帧率控制较为不准。本版源码将解码显示过程拆分为两个线程:定时刷新线程 + 解码主线程,定时刷新线程按计算出的帧率发送自定义 SDL 事件,通知解码主线程解码主线程收到 SDL 事件后,获取一个视频帧解码并显示。
3. 编译与验证
3.1 编译
在源码目录运行:
./compiler.sh
编译后在当前目录生成可执行程序 ffplayer。
3.2 验证
选用 ffmpeg_training/video/clock.avi
作为测试文件。
查看视频文件格式信息:
think@stone-suse> ffprobe -hide_banner clock.avi
[avi @ 0xe7cb40] non-interleaved AVI
Input #0, avi, from 'video/clock.avi':
Duration: 00:00:12.00, start: 0.000000, bitrate: 42 kb/s
Stream #0:0: Video: msrle ([1][0][0][0] / 0x0001), pal8, 320x320, 1 fps, 1 tbr, 1 tbn
Stream #0:1: Audio: truespeech ([34][0][0][0] / 0x0022), 8000 Hz, mono, s16, 8 kb/s
运行测试命令:
./ffplayer clock.avi
可以听到每隔 1 秒时钟指针跳动一格,跳动 12 次后播放结束。播放过程只有图像,没有声音。播放正常。
4. 参考资料
[1] 雷霄骅,视音频编解码技术零基础学习方法
[2] 雷霄骅,FFmpeg源代码简单分析:常见结构体的初始化和销毁(AVFormatContext,AVFrame等)
[3] 雷霄骅,最简单的基于FFMPEG+SDL的视频播放器ver2(采用SDL2.0)
[4] 雷霄骅,最简单的视音频播放示例7:SDL2播放RGB/YUV
[5] 使用SDL2.0进行YUV显示
[6] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 01: Making Screencaps
[7] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 02: Outputting to the Screen
[8] YUV图像里的stride和plane的解释
[9] 图文详解YUV420数据格式
[10] YUV,https://zh.wikipedia.org/wiki/YUV
5. 修改记录
2018-11-23 V1.0 初稿
2018-11-29 V1.1 增加定时刷新线程,使解码帧率更加准确
2019-01-12 V1.2 增加解码及显示过程说明
2025-04-02 V1.3 FFmpeg 版本 4.2.2 升级至 7.1.1