Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

Yveltals Blog

FFmpeg开发

时间基与时间戳

关联:

  • 时间基 (time_base) 乘以时间戳 (timestamp) 即为实际时刻(单位s)
  • 如:总时长 = duration * av_q2d(AV_TIME_BASE_Q)
  • 如:显示时间 = PTS * av_q2d(stream->time_base)

时间戳:

  • PTS 显示时间戳,DTS 解码时间戳

时间基:

  • FFmpeg内部时间基:
    • 用于FFmpeg内部计算,AV_TIME_BASE = 10^6,AV_TIME_BASE_Q为其倒数
  • 编解码器中的时间基tbc
    • 对应AVCodecContext,根据帧率来设定如25帧,则{num = 1,den=25}
  • 容器中的时间基tbn
    • 对应AVStream,根据采样频率设定如 (1,90000),是AVPacket中PTS和DTS的单位
    • 确认输入输出流的时间基
      avformat_find_stream_info:获得输入流时间基
      avformat_write_header:根据输出文件封装格式确定输出流时间基
    • 输入输出时间基不同,需要转换
      av_packet_rescale_ts() :将 AVPacket 根据时间基重新计算时间戳PTS、DTS

时间基转换:

  • 视频按帧进行播放,所以原始视频帧时间基为 1/framerate。视频解码前需要处理输入 AVPacket 中各时间参数,将输入容器中的时间基转换为 1/framerate 时间基;视频编码后再处理输出 AVPacket 中各时间参数,将 1/framerate 时间基转换为输出容器中的时间基。
  • 音频按采样点进行播放,所以原始音频帧时间为 1/sample_rate。音频解码前需要处理输入 AVPacket 中各时间参数,将输入容器中的时间基转换为 1/sample_rate 时间基;音频编码后再处理输出 AVPacket 中各时间参数,将 1/sample_rate 时间基转换为输出容器中的时间基。如果引入音频 FIFO,从 FIFO 从读出的音频帧时间戳信息会丢失,需要使用 1/sample_rate 时间基重新为每一个音频帧生成 pts,然后再送入编码器。
    1
    2
    3
    4
    // 解码前的时间基转换
    av_packet_rescale_ts(ipacket, sctx->i_stream->time_base, sctx->o_codec_ctx->time_base);
    // 编码后的时间基转换
    av_packet_rescale_ts(&opacket, sctx->o_codec_ctx->time_base, sctx->o_stream->time_base);

工具函数

1
2
3
4
5
6
7
8
//获取音频样本大小(单个声道)
av_get_bytes_per_sample()
//获取音频声道数量
av_get_channel_layout_nb_channels()
//获取视频帧大小
av_image_get_buffer_size()
//计算重采样输出样本数
av_rescale_rnd()

主要流程

解码过程

  1. 连接和打开视频流
    • avformat_network_init:初始化并启动TLS库,用来打开网络流
    • avformat_open_input:为AVFormatContext分配空间、打开输入媒体流、探测封装格式、读数据文件头、创建AVStream
  2. 定位视频流数据
    • avformat_find_stream_info:该函数内部已经做了一套完整的解码流程,获取了多媒体流的信息
  3. 准备解码器codec
    • avcodec_find_decoder:寻找解码器
    • avcodec_alloc_context3:创建 AVCodecContext
    • avcodec_parameters_to_context:内容拷贝,流的参数
    • avcodec_open2:打卡解码器,分配相关变量内存、检查解码器状态等。
  4. 解码
    • av_read_frame:取出一个完整帧,包括压缩后的流数据和附加信息
    • avcodec_send_packet:解码,按dts递增顺序输入
    • avcodec_receive_frame:从缓存或解码器内存中取出解压数据,按pts递增顺序输出
    • sws_scale:进行尺寸缩放和转码工作
    • av_packet_unref:解除引用
      刷新缓冲区:avcodec_send_packet传一个空packet,再解码就能拿到剩余帧数据

编码流程

  1. 打开输出文件
    • avformat_alloc_output_context2
    • avio_open
  2. 查找编码器、创建输出流
    • avcodec_find_encoder
    • avformat_new_stream:设置时间基和编码参数 codecpar(宽、高)
    • avcodec_parameters_to_context:参数拷贝到编码器上下文,还需设置时间基、格式、码率
    • avcodec_open2
  3. 写文件头 avformat_write_header
  4. 编码
    • 读帧
    • avcodec_send_frame
    • avcodec_receive_packet
    • av_packet_rescale_ts:重新计算时间戳
    • av_interleaved_write_frame:交替写入帧
  5. 写文件尾 av_write_trailer

音频流视频流混合进输出媒体时,需要确保音频帧和视频帧按照 dts 递增的顺序交错排列,这就是交织问题

av_interleaved_write_frame() 函数会缓存一定数量的帧,将缓存的帧按照 dts 递增的顺序写入输出媒体,调用者不必关注交织问题(小范围的 dts 顺序错误问题这个函数可以修正)

av_write_frame() 函数会直接将帧写入输出媒体,用户必须自行处理交织问题

转码流程


FFmpeg源码解析

初始化

avformat_open_input() :分配空间、打开输入媒体流、探测封装格式、读数据文件头、创建AVStream

  • 分配AVFormatContext、设置options
  • init_input():打开输入媒体流、探测封装格式
    • 当使用自定义AVIOContext时(s->pb!=NULL),如果指定了AVInputFormat就直接返回,否则调用av_probe_input_buffer2()推测AVInputFormat(如从内存中读取数据的时候)
    • 如果指定了AVInputFormat就直接返回,否则根据文件路径推测AVInputFormat,调用av_probe_input_format2()
      • read_probe、av_match_ext、av_match_name匹配得分
    • 如果路径判断不出文件格式,则调用avio_open2()打开文件,再用av_probe_input_buffer2()推测AVInputFormat
  • s->iformat->read_header():读取多媒体数据文件头
    • avformat_new_stream():根据视音频流创建相应的AVStream
  • 拷贝白名单与黑名单协议、读取ID3V2参数

avformat_alloc_output_context2() :分配空间、设默认值、根据文件名猜AVOutputFormat

  • avformat_alloc_context()
    • av_opt_set_defaults()
  • 若参数指定了AVOutputFormat,则直接赋值给oformat
  • 否则调用av_guess_format()遍历所有AVOutputFormat,计算匹配度
    • av_match_name():封装格式名匹配,score加100
    • strcmp():mime类型匹配,score加10
    • av_match_ext():如果文件名称的后缀匹配,score加5

avio_open2() :根据文件名找URLProtocol,再用对应协议打开(file、rtmp),初始化AVIOContext

  • ffurl_open_whitelist():初始化URLContext
    • ffurl_alloc()
      • url_find_protocol():根据文件路径查找合适的URLProtocol
      • url_alloc_for_protocol():为URLProtocol创建URLContext
    • ffurl_connect()
      • url_open()/url_open2():打开URLProtocol(根据协议调用file_open()rtmp_open()等)
  • ffio_fdopen():根据URLContext初始化AVIOContext
    • avio_alloc_context()

avformat_find_stream_info() :做了一套完整的解码流程,获取了多媒体流的信息

  • find_decoder()
    • avcodec_find_decoder():获取解码器
  • avcodec_open2()
  • read_frame_internal():读取完整的一帧压缩编码的数据
  • try_decode_frame():解码一些压缩编码数据
  • has_codec_parameters():检查AVStream成员变量是否设置完毕
  • estimate_timings():通过PTS、已知流时长或码率估算AVFormatContext及AVStream的duration

avcodec_open2()

  • 通过各种av_malloc()分配结构体
  • 将AVDictionary选项设置到AVCodecContext
  • 各种检查,比如检查编解码器是否处于“实验”阶段
  • 如果是编码器,检查输入参数是否符合编码器的要求
  • 调用AVCodec的init()初始化具体的解码器

读写帧

av_parser_parse2()

av_read_frame()

把文件拆分为若干个帧,每次调用返回一帧数据包,但不校验是否有效

返回的数据包被引用计数,必须使用av_packet_unref()进行释放。

如果音频是可变大小,则只包含一帧。

如果视频中存在B帧,pkt->pts可能为AV_NOPTS_VALUE,所以最好使用pkt->dts作为依赖。

  • avpriv_packet_list_get()
  • av_read_frame_internal()
    • ff_read_packet()
      • iformat->read_packet():调用AVInputFormat指向的read_packet()读取数据包
    • parse_packet()
      • av_parser_parse2():解析出视频一帧(音频若干帧)

avformat_write_header() :初始化复用器、检查AVStream的time_base,采样率,宽高、写入封装头

  • init_muxer():初始化复用器
    • 将AVDictionary选项设置到AVFormatContext
    • 遍历AVFormatContext中的每个AVStream,并检查:
      • time_base是否正确设置,否则调用avpriv_set_pts_info()进行设置
      • 音频采样率,视频宽、高、宽高比
  • AVOutputFormat->write_header():写入相应封装头

av_write_frame() / av_interleaved_write_frame() :检查 stream_index, codec_type, pts, dts,调用write_packets_common()写入帧,interleaved参数传1交错写入,总会调用 av_packet_unref() 解引用

write_packets_common()

  • 检查pkt的stream_index和codec_type
  • 检查pkt的pts和dts
  • 调用s->oformat->check_bitstream()检查码流
  • 调用函数二选一写数据包
    • write_packets_from_bsfs():经过bitstream filter处理,如h264要处理startcode起始码
    • write_packet_common()
      • 若interleaved标志位交错,调用interleave_packet()排序
      • s->oformat->write_packet():校正时间戳,根据封装格式写入未/压缩编码的数据包

av_write_trailer()

  • 如果有AVBSFContext,最后写入bitstream filter数据包
  • 填充空数据包
  • 如果有AVIOContext,avio_write_marker写入marker标志
  • s->oformat->write_trailer()写文件尾
  • deinit_muxer():释放muxer资源
  • 释放priv_data和index_entries

编码

avcodec_send_frame() :检查编码器、frame,不对frame赋值

  • 编码器是否打开、是否为编码器
  • encode_send_frame_internal():解析音频metadata、检查frame是否有效
  • 真正frame的赋值操作在后续ff_encode_get_frame()

avcodec_receive_packet() :检查编码器、w,h,f、取出未压缩的一帧

  • 编码器是否打开、是否为编码器
  • encode_receive_packet_internal():检测视频宽高、像素格式,判断调用二选一
    • receive_packet()
    • 或encode_simple_receive_packet()->encode_simple_internal()
      • frame为空则调用ff_encode_get_frame()取出一帧未压缩的数据
      • ff_thread_video_encode_frame或avctx->codec->encode2()编码

转码

sws_getContext()

sws_scale


FFmpeg结构体

分类

  1. 解协议(http,rtsp,rtmp)

    • AVIOContext:管理输入输出数据,硬盘数据读到其buffer中再送给解码器
      • URLContext:对具体资源文件进行操作的上下文
        • URLProtocol:广义的输入文件(文件、网络数据流等),每种协议都对应一个URLProtocol
  2. 解封装(flv,avi,rmvb,mp4)

    • AVFormatContext:存储视音视频封装格式中包含的信息
      • AVInputFormat、AVOutputFormat:对应一种封装格式
      • AVIOContext
      • AVStream
      • AVDictionary:元数据
  3. 解码(h264,mpeg2,aac,mp3)

    • AVStream:存储每一个视频/音频流信息的结构体
      • AVCodecContext:编解码器上下文结构体,存储音视频流的解码方式的相关数据
        • AVCodec:每种编解码器对应一个该结构体(如ff_h264_decoer)
        • AVRational,宽高,声道数,采样率
        • profile,level
  4. 存数据 对于视频,每个结构一般是存一帧;音频可能有好几帧

    • AVPacket
    • AVFrame

AVPacket

  • uint8_t* data:本身不包含压缩的帧数据,而是指向缓存空间
  • int size
  • AVBufferRef *buf:对data指向的数据缓冲区引用管理
    • av_packet_ref缓存空间引用+1,复制packet中的其他字段,仅首次将src->data复制到buf->buffer中
    • av_packet_unref引用-1,变为0时释放缓存空间。
  • ptsdts
  • stream_index
  • int key_frame:是否关键帧
  • duration(解码后帧播放时长)

AVFrame

AVFrame 通常只需分配一次,然后可以多次重用,每次重用前应调用 av_frame_unref() 将 frame 复位到原始的干净可用的状态

  • uint8_t *data[]:原始帧存在若干plane中,对应planar、packet格式和单双声道
  • int linesize[]:对齐填充,可能比实际原数据大
  • AVBufferRef *buf[]
  • uint8_t **extended_data:存多声道音频
  • width, height
  • nb_samples, channel_layout
  • int format:帧格式AV_PIX_FMT_YUV420P、AV_SAMPLE_FMT_S16
  • int key_frame:是否关键帧
  • 宏块类型表、运动矢量表等

FFmpeg相关文章

Tools:MediaInfo, VLC media player, wireshark, Elecard StreamEye Tools

FFmpeg时间戳详解

分析视频流

YUV结构详解

AVBuffer 和 AVBufferRef

AVFrame几种分配堆空间的方式

AVStream中codec_tag初始化0

av_seek_frame()


FFmpeg指令

分解与复用

1.格式转换

1
2
## 音视频编码处理方式都是copy,不改变
ffmpeg -i gfxm.mp4 -vcodec copy -acodec copy out.flv

2.抽取视频

1
2
## -an表示不需要音频数据
ffmpeg -i input_file -vcodec copy -an output_file_video

3.抽取音频

1
2
## -vn表示不需要视频数据
ffmpeg -i input_file -acodec copy -vn output_file_audio

处理原始数据

1.提取YUV数据

1
2
3
4
## -c:v 表示对v(视频)进行c(编码), -pix_fmt 指定了视频的像素格式
ffmpeg -i gfxm.mp4 -an -c:v rawvideo -pix_fmt yuv420p out.yuv
#大小通过提取原始数据时产生的参数获取(在input中)
ffplay out.yuv -s 864x486

2.提取PCM数据

1
2
3
4
## -ar采样率 -ac双声道 -f:PCM数据存储格式 s有符号 16位 le小端存储
ffmpeg -i gfxm.mp4 -vn -ar 44100 -ac 2 -f s16le out.pcm
## 对于PCM播放时,我们要制定ar,ac,f参数
ffplay out.pcm -ac 44100 -ac 2 -f s16le

音视频裁剪

1.裁剪

1
2
## -ss开始时间 -t时间长度(单位s)
ffmpeg -i gfxm.mp4 -ss 00:01:00 -t 10 out.ts

2.合并

1
2
## 文件片段为.ts
ffmpeg -f concat -i input.txt out2.flv

input.txt

1
2
file "out.ts"
file "out2.ts"

图片视频互转

1.视频转图片

1
2
## -r 指定转换图片频率(每秒转出几张) -f 指定转出图片格式image2
ffmpeg -i out.ts -r 1 -f image2 image-%3d.jpeg

2.图片转视频

1
2
#可以通过-r指定每秒放的帧数
ffmpeg -i image-%3d.jpeg out5.mp4