ffmpeg实现播放器

前言

逐步深入多媒体技术。

前置知识补充

除了前篇必会的
https://xn--74q78i15hxv3arigm4e.cn/2019/04/23/FFmpeg-%E5%9F%BA%E7%A1%80%E7%90%86%E8%AE%BA%E4%B8%8E%E5%85%A5%E9%97%A8/
https://xn--74q78i15hxv3arigm4e.cn/2019/04/05/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E7%8E%A9%E7%8E%A9%E5%A4%9A%E5%AA%92%E4%BD%93%E5%91%A2/

接下来介绍些开发中需要用到的前置

PPM格式

一种简单的图像格式
头:
pn\n表示内容与格式
图像宽度 高度\n
最大像素值\n 0-255
后面直接按pn的要求存图像数据即可

SDL

wiki: https://zh.wikipedia.org/wiki/SDL
SDL(Simple DirectMedia Layer)开源的跨平台多媒体开发库。
其跨平台方式是源码跨平台,分发不同平台的链接库

其目的是精简控制图像声音与输入工具所需要的代码,因此其实现基本为各个平台系统库的再包装。
如win上为directX、linux为Xlib。此外还提供了很多功能函数库(网络等)

openGL

跨语言、平台的专注图形渲染库,图形加速硬件需要厂商提供驱动实现。

directX

linux下的驱动:硬件->驱动实现->注册为设备(字符设备、块设备)->成为文件系统->对外系统调用接口(read\write)

微软系统专为多媒体以及游戏开发的应用程序接口,硬件厂商需要为每款硬件产品写DX驱动。

PTS、DTS

视编码中,并不是每一帧都是完整的画面,处理分为:
I帧:仅利用单帧图像内的空间相关性压缩
P帧:空间与时间都参考,向前时间参考
B帧:空间与时间都参考,双向时间参考,B帧不可作为参考

DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。

当没有b帧时,二者通常一致,有b帧时,DTS告诉解码顺序,PTS告诉显示顺序
采集顺序与显示顺序相同。编码顺序、传输顺序和解码顺序相同。

指令架构AMD64、Intel64、x64等等兼容x86的都是AMD64指令集…intel商业混淆了半天起了一堆名字….

FreeBSD也是开源的类unix系统,ps4使用其内核。

音频转码也叫重采样

解复用(demux),表示从一路输入中分离出多路流(视频、音频、字幕等)。
复用(mux),是multiplex的缩写,表示将多路流(视频、音频、字幕等)混入一路输出中(普通文件、流等)。

FFmpeg使用

可直接使用的库

libavcodec 多媒体编解码器库
libavdevice 设备库
libavfilter 滤镜库
libavformat 媒体格式库
libavutil 实用工具
libpostproc 后处理库
libswresample 音频重采样库
libswscale 媒体放缩

FFmpeg可以识别5种流类型:音频(audio, a),视频(video, v),字幕(subtitle, s),附加数据(attachment, t)和普通数据(data, d)。容器可含很多种不同的流。

内存IO与外部IO

所谓内存IO,在FFmpeg中叫作“buffered IO”或“custom IO”,指的是将一块内存缓冲区用作FFmpeg的输入或输出。与内存IO操作对应的是指定URL作为FFmpeg的输入或输出,比如URL可能是普通文件或网络流地址等。

步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// @opaque : 用户自定义参数
// @buf : 输出buf缓存给ffmpeg-就是avio_alloc_context的参1
// @buf_size: buf的大小
// @return : 本次IO数据量
static int read_packet(void *opaque, uint8_t *buf, int buf_size)
{
//从自己定义的参数里取数据向buffer里填就行
int fd = *((int *)opaque);
int ret = read(fd, buf, buf_size);
return ret;
}
int main()
{
size_t ibuf_size = 4096;
// 打开一个FIFO文件的读端
int fd = open_fifo_for_read("/tmp/test_fifo");
// 1. 分配缓冲区--用于FFmpeg处理数据
uint8_t *ibuf = av_malloc(ibuf_size);
// 2. 分配AVIOContext,第三个参数write_flag为0
// FFmpeg使用的缓冲区、大小、对于FFmpeg是出|入(1|0)、用户自定义数据、读写指针、seek函数
AVIOContext *avio_in = avio_alloc_context(ibuf, ibuf_size, 0, &fd, &read_packet, NULL, NULL);
// 3. 分配AVFormatContext,并指定AVFormatContext.pb字段。必须在调用avformat_open_input()之前完成,指定AV格式环境的流来源,open进一步填写时直接NULL来源即可
AVFormatContext *ifmt_ctx = avformat_alloc_context();
ifmt_ctx->pb = avio_in;//avio_open()可打开URL为AVIOContext
// 4. 打开输入(读取封装格式文件头),忽略URL参数则使用内存模式(ifmt_ctx为null则自动分配)
avformat_open_input(&ifmt_ctx, NULL, NULL, NULL);
......
//之后继续对AVFormatContext填充获取信息
}
* | buffer_size |
* |---------------------------------------|
* | |
*
* buffer buf_ptr buf_end
* +---------------+-----------------------+
* |/ / / / / / / /|/ / / / / / /| |
* read buffer: |/ / consumed / | to be read /| |
* |/ / / / / / / /|/ / / / / / /| |
* +---------------+-----------------------+
*
* pos
* +-------------------------------------------+-----------------+
* input file: | | |
* +-------------------------------------------+-----------------+

输出使用类似,只要提供适量缓存就可以将流式结构型文件解析出来。

AVBuffer、AVFrame、AVPacket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_______ ______________
| | | |
| input | demuxer | encoded data | decoder
| file | ---------> | packets | -----+
|_______| |______________| |
v
_________
| |
| decoded |
| frames |
|_________|
________ ______________ |
| | | | |
| output | <-------- | encoded data | <----+
| file | muxer | packets | encoder
|________| |______________|

AVBuffer为FFmpeg中使用的缓冲区,使用引用计数管理
AVFrame中存储的是经过解码后的原始数据
AVPacket中存储的是经过编码的压缩数据。

AVFrame:
data是一个指针数组,数组的每一个元素是一个指针,指向视频中图像的各个plane或音频中的各个plane。交织时如YUVYUV,指向这一个plane即可。
对于视频来说,linesize是每行图像的大小(字节数)。注意有对齐要求。
对于音频来说,linesize是每个plane的大小(字节数)。音频只使用linesize[0]。对于planar音频来说,每个plane的大小必须一样。
width, height视频帧宽和高(像素)。
format帧格式
nb_samples音频帧中单个声道中包含的采样点数
pict_type视频帧类型(I、B、P等)
sample_aspect_ratio视频帧的宽高比。
pts显示时间戳。单位是time_base。
pkt_pts此frame对应的packet中的显示时间戳。
pkt_dts此frame对应的packet中的解码时间戳。
coded_picture_number在编码流中当前图像的序号。
display_picture_number在显示序列中当前图像的序号。
sample_rate音频采样率。
channel_layout音频声道布局。每bit代表一个特定的声道
alloc分配、free释放、ref拷贝并计数

AVPacket:
对于视频而言,一个AVPacket通常只包含一个压缩视频帧。而对于音频而言,一个AVPacket可能包含多个完整的音频压缩帧。编码结束后只需要更新一些参数时就可以发空packet。
AVPacket对象可以在栈上分配,注意此处指的是AVPacket对象本身。而AVPacket中包含的数据缓冲区是通过av_malloc()在堆上分配的。
pts,显示时间戳
dts,解码时间戳
duration,当前包解码后的帧播放持续的时长。单位timebase。值等于下一帧pts减当前帧pts。
pos,流中的位置

time_base。pts与dts的基本单位,AVStream中精度高,AVCodecContext中为1/FPS。
AVPacket下的pts和dts以AVStream->time_base为单位
AVFrame里面的pkt_pts和pkt_dts是拷贝自AVPacket,同样以AVStream->time_base为单位;而pts是为输出(显示)准备的,以AVCodecContex->time_base为单位)。

滤镜filter

滤镜filter用于修改未编码的原始音视频数据,多个滤镜可以连接起来
FFmpeg比较常用的滤镜有:scale、trim、overlay、rotate、movie、yadif。scale滤镜用于缩放,trim滤镜用于帧级剪切,overlay滤镜用于视频叠加,rotate滤镜实现旋转,movie滤镜可以加载第三方的视频,yadif滤镜可以去隔行。

编解码

解码使用avcodec_send_packet()和avcodec_receive_frame()两个函数。
编码使用avcodec_send_frame()和avcodec_receive_packet()两个函数。

关于avcodec_send_packet()与avcodec_receive_frame()的使用说明:

  1. 按dts递增的顺序向解码器送入编码帧packet,解码器按pts递增的顺序输出原始帧frame,实际上解码器不关注输入packet的dts(错值都没关系),它只管依次处理收到的packet,按需缓冲和解码
  2. avcodec_receive_frame()输出frame时,会根据各种因素设置好frame->best_effort_timestamp(文档明确说明),实测frame->pts也会被设置(通常直接拷贝自对应的packet.pts,文档未明确说明)用户应确保avcodec_send_packet()发送的packet具有正确的pts,编码帧packet与原始帧frame间的对应关系通过pts确定
  3. avcodec_receive_frame()输出frame时,frame->pkt_dts拷贝自当前avcodec_send_packet()发送的packet中的dts,如果当前packet为NULL(flush packet),解码器进入flush模式,当前及剩余的frame->pkt_dts值总为AV_NOPTS_VALUE。因为解码器中有缓存帧,当前输出的frame并不是由当前输入的packet解码得到的,所以这个frame->pkt_dts没什么实际意义,可以不必关注
  4. avcodec_send_packet()发送第一个NULL会返回成功,后续的NULL会返回AVERROR_EOF。
  5. avcodec_send_packet()多次发送NULL并不会导致解码器中缓存的帧丢失,使用avcodec_flush_buffers()可以立即丢掉解码器中缓存帧。因此播放完毕时应avcodec_send_packet(NULL)来取完缓存的帧,而SEEK操作或切换流时应调用avcodec_flush_buffers()来直接丢弃缓存帧。
  6. 解码器通常的冲洗方法:调用一次avcodec_send_packet(NULL)(返回成功),然后不停调用avcodec_receive_frame()直到其返回AVERROR_EOF,取出所有缓存帧,avcodec_receive_frame()返回AVERROR_\EOF这一次是没有有效数据的,仅仅获取到一个结束标志。

关于avcodec_send_frame()与avcodec_receive_packet()的使用说明:

  1. 按pts递增的顺序向编码器送入原始帧frame,编码器按dts递增的顺序输出编码帧packet,实际上编码器关注输入frame的pts不关注其dts,它只管依次处理收到的frame,按需缓冲和编码
  2. avcodec_receive_packet()输出packet时,会设置packet.dts,从0开始,每次输出的packet的dts加1,这是视频层的dts,用户写输出前应将其转换为容器层的dts
  3. avcodec_receive_packet()输出packet时,packet.pts拷贝自对应的frame.pts,这是视频层的pts,用户写输出前应将其转换为容器层的pts
  4. avcodec_send_frame()发送NULL frame时,编码器进入flush模式
  5. avcodec_send_frame()发送第一个NULL会返回成功,后续的NULL会返回AVERROR_EOF
  6. avcodec_send_frame()多次发送NULL并不会导致编码器中缓存的帧丢失,使用avcodec_flush_buffers()可以立即丢掉编码器中缓存帧。因此编码完毕时应使用avcodec_send_frame(NULL)来取完缓存的帧,而SEEK操作或切换流时应调用avcodec_flush_buffers()来直接丢弃缓存帧。
  7. 编码器通常的冲洗方法:调用一次avcodec_send_frame(NULL)(返回成功),然后不停调用avcodec_receive_packet()直到其返回AVERROR_EOF,取出所有缓存帧,avcodec_receive_packet()返回AVERROR_EOF这一次是没有有效数据的,仅仅获取到一个结束标志。
  8. 对音频来说,如果AV_CODEC_CAP_VARIABLE_FRAME_SIZE(在AVCodecContext.codec.capabilities变量中,只读)标志有效,表示编码器支持可变尺寸音频帧,送入编码器的音频帧可以包含任意数量的采样点。如果此标志无效,则每一个音频帧的采样点数目(frame->nb_samples)必须等于编码器设定的音频帧尺寸(avctx->frame_size),最后一帧除外,最后一帧音频帧采样点数可以小于avctx->frame_size

流媒体

将媒体数据压缩并封装成为流

FFmpeg中若URL携带“rtmp://”、“rpt://”、“udp://”等前缀,则表示涉及流处理
分别使用对应的协议

实现播放器

VS通用c++配置方式

头文件搜索:项目属性、c++中添加附加路径。若使用脚本集成编译则将库头文件复制到项目中添加或使用相对vcxproj添加
库文件添加:项目属性、连接器中添加附加路径(lib放进项目时没加也能编)、代码&配置中添加链接目标。若使用脚本集成编译环境则将lib复制进项目中一起编译
动态库添加:动态库需要放于应用程序同级便于搜索,也可添加path。编译时加lib用于启动时加载dll。也可全手动加载

基本过程

容器->流*n->包->帧

流程熟悉

简单实现官方教程,但由于接口过时且说的不够详细,所以仅作为流程了解,核心的参考为之后收集的博客实现

官方实现:https://github.com/mpenkov/ffmpeg-tutorial

新版本实现:(基于https://www.cnblogs.com/leisure_chn/p/10284653.html)
https://github.com/imbaya2466/FFmpeg-play

参考

  1. https://www.cnblogs.com/leisure_chn/category/1351812.html
  2. https://blog.csdn.net/leixiaohua1020/article/details/15811977
  3. https://www.cnblogs.com/leisure_chn/p/10584910.html
  4. http://dranger.com/ffmpeg/