[关闭]
@1405010304geshuaishuai 2018-03-23T15:32:44.000000Z 字数 9760 阅读 868

ffmpeg + SDL 实现简单播放器

音视频 ffmpeg


当前进度

第一个月整体进度

第一周进度
<2.23-3.1>1周
1、使用FFmpeg进行将视频解码显示在QImage中(done)
2、使用FFmpeg&YUV(SDL)播放视频(每一帧图像采用延时40ms)(done)
3、使用FFmpeg&YUV进行MP3文件的播放(done)
<3.4>1天
4、使用FFmpeg进行视频同步,不是采用每张图片延时40ms(done)
<3.5-3.18>2周
5、根据tutorial3进行编码发现播放mp3的时候有杂音
6、发现播放mp3杂音的原因并寻找解决办法(done)
7、发现QImage播放视频感觉视频画质有所下降,将SDL放入QWidget中(done)
8、最终简单播放器Demo出版(done)
<3.19-3.26>1周
9、增加时间进度条的显示(done)
10、通过时间进度条来进行seek(doing...)
11、增加打开文件功能和播放暂停的功能(done)

1. ffmpeg

FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多代码都是从头开发的。

多媒体视频处理工具FFmpeg有非常强大的功能包括视频采集功能、视频格式转换、视频抓图、给视频加水印等。

2. 项目组成

  1. libavcodec: 用于各种类型声音/图像编解码;
  2. libavutil: 包含一些公共的工具函数;
  3. libavformat: 包含多种多媒体容器格式的封装、解封装工具;
  4. libswscale: 用于视频场景比例缩放、色彩映射转换;
  5. libpostproc: 用于后期效果处理;
  6. libavdevice: 用于音视频数据采集和渲染等功能的设备相关;
  7. libswresample: 用于音频重采样和格式转换等功能;
  8. libavfilter: 包含多媒体处理常用的滤镜功能;

3.多媒体处理基本流程

最核心--视频解码过程

Created with Raphaël 2.1.2StartMedia fileDemux(解复用)解码(Decode)YUV/RGB 数据End

1、解复用
我们知道在一个多媒体文件中,既包括音频也包括视频,而且音频和视频都是分开进行压缩的,因为音频和视频的压缩算法不一样,及让压缩算法不一样,那么解码可定也不一样,所以需要对音频和视频分别进行解码。虽然音频和视频是分开进行压缩的,但是为了传输方便,将压缩的音频和视频捆绑在一起进行传输。所以我们解码的第一步就是将这些绑定在一起的音频和视频分开来,也就是解复用。解复用这一步就是将文件中捆绑在一起的音频流和视频流分开来以方便分别对它们进行解码,下面是Demux之后的效果。

OneTwomedia filevideo streamsaudio streams

2、解码(Decode)
一个多媒体文件可定是经过某种或几种格式的压缩的,也就是通常所说的视频和音频编码,编码是为了减少数据量,否则的话对我们的存储设备是一个挑战,如果是流媒体的话对网络宽带也是一个几乎不可能完成的任务。所以我们必须对媒体信息进行尽可能的压缩。

3、FFmpeg中解码流程对应的API函数
FFmpeg中Demux这一步是通过avformat_open_input()这个api来做的,这个api读出文件的头部信息,并做demux,在此之后我们就可以读取媒体文件中的音频和视频流,然后通过av_read_frame()从音频和视频流中读取出基本数据流packet,然后将packet送到avcodec_decode_video2()和相对应的api进行解码。

4.关键函数

avformat_open_input() 该函数用于打开多媒体数据并获得一些相关的信息。它的声明位于libavformat\avformat.h,如下所示。

  1. int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options);

ps: 函数调用成功后处理过的AVFormatContext结构体。
file: 打开的视音频流的URL。
fmt: 强制指定AVFormatContext中AVInputFormat的。这个参数一般情况下可以设置为NULL,这样FFmpeg可以自动检测AVInputFormat。
dictionary: 附加的一些选项,一般情况下可以设置未NULL。

两个关键的函数:
init_input():绝大部分初始化工作是在这里做的
s->iformat->read_header():读取多媒体数据头文件,根据视音频创建相应的AVStream.

init_input()的主要工作就是打开输入的视频数据并且探测视频的格式。

调用完init_input()完成基本的初始化并且退测得到相应的AVInputFormat之后,avformat_open_input()会调用AVInputFormat的read_header()方法读取媒体文件的文件头并且完成相关的初始化工作。
以FLV为例,read_header()指向了flv_read_header()函数。flv_read_header()函数都去了FLV的文件头并且判断其中是否包含视频流和音频流。如果包含视频流或者音频流,就会调用create_stream()函数。
AVInputFormat的read_header()完成了视音频对应的AVStream的创建。

avformat_find_stream_info()

该函数主要用于给每个媒体流(音频/视频)的AVStream结构体赋值。

avcodec_find_decoder()

用于查找FFMPEG的解码器。
函数的参数是一个解码器的ID,返回查找到的解码器(没有找到就返回NULL)。

avcodec_open2()

该函数用于初始化一个视音频编解码器的AVCodecContext.

av_read_frame()

作用是读取码流中的音频若干帧或者视频一帧。解码视频的时候,每解码一个视频帧,需要先调用av_read_frame()获得一帧视频的压缩数据,然后才能对该数据进行解码(例如H.264中一帧压缩数据通常对应一个NAL)。

  1. int av_read_frame(AVFormatContext *s, AVPacket *pkt);

s:输入的AVFormatContext
pkt: 输出的AVPAcket
如果返回0则说明读取正常。

avcodec_decode_video2()

  1. int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture, int *got_picture_ptr, const AVPacket *avpkt);

作用是解码一帧视频数据,输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame。该函数的声明位于libavcodec\avcodec.h

5、SDL播放音频
SDL播放音频采用回调函数的方式来保证音频的连续性,在设置音频输出参数,向系统注册回调函数后,每一次写入的音频数据播完后,系统自动调用注册的回调函数,通常在此回调函数中继续往系统写入音频数据。
SDL播放音频函数调用序列,忽略掉错误处理:

  1. 1)初始化SDL_AudioSpec结构,此结构包括音频参数和回调函数,比如SDL_AudioSpec wanted_spec;wanted_spec.userdata = is; wanted_spec.channels = 2; wanted_spec.callback = sdl_audio_callback;
  2. 2)打开音频设备SDL_OpenAudio(&wanted_spec,&spec);
  3. 3)激活音频设备开始工作 SDL_PauseAudio(0);
  4. 4)在音频回调函数中写入音频数据,示意代码如下:
  5. void sdl_audio_callback(void *opaque, uint8*stream, int len)
  6. {
  7. memcpy(stream,(uint8_t*)audio_buf,len);
  8. }
  9. 5)播放完后关闭音频SDL_CloseAudio();

5.At its very basic level, dealing with video and audio streams is very easy:

10 OPEN Video_stream FROM video.avi
20 READ packet FROM video_stream INTO frame
30 IF frame NOT COMPLETE GOTO 20
40 DO SOMETHING WITH frame
50 GOTO 20

6.结构体分析

FFMPEG有几个最重要的结构体,包含了解协议,解封装,解码操作,存数据。

1、解协议(http,rtsp,rtmp,mms)
AVIOContext, URLProtocol, URLContext主要存储视音频使用的协议的类型以及状态。
URLProtocol存储输入视音频使用的封装格式。每种协议都对应一个URLProtocol结构。
AVIOContext->URLContext->URLProtocol ProtocolLayer

2、解封装(flv,avi,rmvb,mp4)
AVFormatContext主要存储音视频封装格式中包含的信息;AVInputFormat存储输入视音频使用的封装格式。每种视音频封装格式都对应一个AVInputFormat结构。
AVFormatContext是包含码流参数较多的结构体。
结构体的定义(位于avformat.h)
在使用FFMPEG进行开发的时候,AVFormatContext是一个贯穿始终的数据结构,很多函数都要用到它作为参数。它是FFMPEG解封装(flv,mp4,rmvb,avi)功能的结构体。下面看几个主要变量的作用(这里考虑解码的情况):
struct AVInputFormat *iformat: 输入数据的封装格式
AVIOContext *pb: 输入数据的缓存
unsigned int nb_streams: 音视频流的个数
AVStream **streams: 音视频流
char filename[1024]: 文件名
int64_t duration: 时长(单位:微妙us,转换为秒需要除以1000000)
int bit_rate:比特率(单位bps,转化为kbps需要除以1000)
AVDictionary *metadata: 元数据

3、解码(h264,mpeg2,aac,mp3)
每个AVStream存储一个视频/音频流的相关数据;每个AVStream对应一个AVCodecContext,存储该视频/音频流使用解码方式的相关数据;AVCodecContext中对应一个AVCodec,包含该视频/音频对应的解码器。每种解码器都对应一个AVCodec结构。
AVStream[0]->AVCodecContext->AVCodec CodecLayer
AVStream[1]->AVCodecContext->AVCodec

3.1 AVStream是存储每一个视频/音频流信息的结构体。(位于avformat.h文件中)

int index: 表示该视频/音频流
AVCodecContext *codec:指向该视频/音频流的AVCodecContext(他们是一一对应的关系)
AVRational time_base: 时基。通过该值可以把PTS,DTS转化为真正的时间。FFMPEG其它结构中也有这个字段,只有AVStream中的time_base是可用的。PTS*time_base=真正的时间。
int64_t duration:该视频/音频流长度
AVDictionary *metadata: 原数据信息
AVRational avg_frame_rate:帧率(对视频,这个挺重要的)
AVPacket attached_pic: 附带图片。比如说一些MP3,AAC音频文件附带的专辑封面。

3.2 AVCodecContext是包含变量较多的结构体。位于(avcodec.h)

AVCodecContext是包含变量较多的结构体。
关键变量分析:
enum AVMediaType codec_type: 编解码器的类型(视频,音频...)
struct AVCodec *codec: 采用的解码器AVCodec(H.264,MPEG2...)
int bit_rate: 平均比特率
uint8_t *extradata; int extradata_size: 针对特定编码器包含的附加信息(例如对于H.264解码器来说,存储SPS,PPS等)
AVRational time_base: 根据该参数,可以把PTS转化为实际的时间(单位为秒s)
int width,height:如果是视频的话,代表宽和高
int refs: 运动估计参考的帧的个数(H.264的话会有多帧,MPEG2这类的一般就没有了)
int sample_rate: 采样率(音频)
int channels:声道数(音频)
enum AVSampleFormat sample_fmt: 采样格式
int profile: 型(H.264里面就有,其他编码标准应该也有)
int level: 级(和profile差不太多)
在这里需要注意:AVCodecContext中很多的参数是编码的时候使用的,而不是解码的时候使用的。

3.3 AVCodec是存储编解码器信息的结构体。(位于avcodec.h文件中)
const char *name:编解码器的名字,比较短
const char *long_name: 编解码器的名字,全程,比较长
enum AVMediaType type: 指明了类型,是视频,音频,还是字幕
enum AVCodecID id: ID,不重复
const AVRational *supported_framerates: 支持的帧率(仅视频)
const enum AVPixelFormat *pix_fmts: 支持的像素格式(仅视频)
const int *supported_samplerates: 支持的采样率(仅音频)
const enum AVSampleFormat *sample_fmts: 支持的采样格式(仅音频)
const uint64_t *channel_layouts: 支持的声道数(仅音频)
int priv_data_size: 私有数据的大小
每个编解码器对应一个该结构体,其中H.264解码器的结构体如下所示(h.264.c):

  1. AVCodec ff_h264_decoder = {
  2. .name = "h264",
  3. .type = AVMEDIA_TYPE_VIDEO,
  4. .id = CODEC_ID_H264,
  5. .priv_data_size = sizeof(H264Context),
  6. .init = ff_h264_decode_init,
  7. .close = ff_h264_decode_end,
  8. .decode = decode_frame,
  9. .capabilities = /*CODEC_CAP_DRAW_HORIZ_BAND |*/ CODEC_CAP_DR1 | CODEC_CAP_DELAY |
  10. CODEC_CAP_SLICE_THREADS | CODEC_CAP_FRAME_THREADS,
  11. .flush= flush_dpb,
  12. .long_name = NULL_IF_CONFIG_SMALL("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"),
  13. .init_thread_copy = ONLY_IF_THREADS_ENABLED(decode_init_thread_copy),
  14. .update_thread_context = ONLY_IF_THREADS_ENABLED(decode_update_thread_context),
  15. .profiles = NULL_IF_CONFIG_SMALL(profiles),
  16. .priv_class = &h264_class,
  17. };

4、存数据
视频的话,每个结构一般是存一帧;音频可能有好几帧
解码前数据:AVPacket
解码后: AVFrame
4.1 AVPacket是存储压缩编码数据相关信息的结构体。
uint8_t *data: 压缩编码的数据
int size: data的大小
int64_t pts:显示时间戳
int64_t dts: 解码时间戳
int stream_index: 标识该AVPacket所属的视频/音频流。
4.2 AVFrame是包含码流参数较多的结构体位于(avcodec.h)
AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),此外还包含一些相关的信息。比如说,解码的时候存储了宏块类型表,QP表,运动矢量等数据。编码的时候也存储了相关的数据。因此在使用FFMPEG进行码流分析的时候,AVFrame是一个很重要的结构体。
uint8_t *data[AV_NUM_DATA_POINTERS]:解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
int linesize[AV_NUM_DATA_POINTERS]:data中“一行”数据的大小。
int width, height:视频帧宽和高(1920x1080,1280x710...)
int nb_samples:音频的一个AVFrame中可能包含多个音频帧,在此标记包含了几个
int format: 解码后原始数据类型(YUV420,YUV422,RGB24...)
int key_frame:是否是关键帧
enum AVPictureType pict_type:帧类型(I,B,P...)
AVRational sample_aspect_ratio:宽高比(16:9,4:3...)
int64_t pts:显示时间戳
int coded_picture_number:编码帧序号
int display_picture_number:显示帧序号
int8_t *qscale_table: QP表
uint8_t *mbskip_table:跳过宏块表
tint16_t(*motion_val[2])[2]:运动矢量表
uint32_t *mb_type:宏块类型表
short *dct_coeff:DCT系数
int8_t *ref_index[2]:运动估计参考帧列表
int interlaced_frame:是否是隔行扫描
uint8_t motion_subsample_log2:一个宏块中的运动矢量采样数
data[]
对于packed格式的数据(例如RGB24),会存到data[0]里面
对于planar格式的数据(例如YUV420P),则会分开成data[0],data[1],data[2]...(YUV420P中data[0]存Y,data[1]存U,data[2]存V)

7、IPB帧及PTS&DTS

6.1、I帧
I帧又称帧内编码帧,时一种自带全部信息的独立帧,无需参考其他图像便可独立进行解码,可以简单理解为一张静态画面。视频序列中的第一帧始终都是I帧,因为它是关键帧。
6.2、P帧
P帧又称帧间预测编码帧,需要参考前面的帧才能进行编码。表示的是当前帧画面与前一帧(前一帧可能是I帧也可能是P帧)的差别。解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。与I帧相比,P帧通常占用更少的数据位,但不足是,由于P帧对前面的P和I帧参考帧有着复杂的依赖性,因此对传输错误非常敏感。
6.3、B帧
B帧又称双向预测编码帧,也就是B帧记录的是本帧与前后帧的差别。也就是说要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是对解码性能要求较高。

总结:
I帧只需要考虑本帧;P帧记录的是与前一帧的差别;B帧记录的是前一帧及后一帧的差别,能节约更多的空间,视频文件小了,但相对来说解码的时候就比较麻烦。因为再解码时,不仅要用之前缓存的画面,而且要知道下一个I或者P的画面,对于不支持B帧解码的播放器容易卡顿。

在视频画面播放过程中,若I帧丢失了,则后面的P帧也就随着解不出来,就会出现视频画面黑屏的现象;若P帧丢失了,则视频画面会出现花屏、马赛克等现象。

PTS:Presentation Time Stamp. PTS主要用于度量解码后的视频帧什么时候被显示出来。
DTS:Decode Time Stamp. DTS 主要时标识读入内存中的bit流在什么时候送入解码器进行解码。

I frame: 自身可以通过视频解压算法解压成一张单独的完整的图片。
P frame: 需要参考其前面的一个I frame或者P frame来生成一张完整的图片。
B frame: 则要参考其前一个I或者P帧及其最后面的一个P帧来生成一张完整的图片。

DTS和PTS的不同
DTS主要用于视频的解码,在解码阶段使用。
PTS主要用于视频的同步和输出,在display的时候用。
在没有B frame的情况下,DTS和PTS的输出顺序是一样的。

I frame的解码不依赖于任何的其他的帧。
P frame的解码则依赖于其前面的I frame或者P frame
B frame的解码则依赖于其前的最近的一个I frame或者P frame及其后的最近的一个P frame.

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注