一、基础知识
FFmpeg:FFmpeg是一个开源的跨平台音视频处理工具集,可以用于录制、转换和流媒体处理等多种音视频操作。它提供了丰富的功能和命令行工具,可以处理几乎所有常见的音视频格式。
使用FFmpeg,您可以执行以下操作:
-
音视频转码:将一个格式的音视频文件转换为其他格式,例如将MP4文件转换为AVI文件。
-
视频剪辑和裁剪:从视频文件中提取特定的片段,或者裁剪视频的画面大小。
-
音频提取和合并:从视频文件中提取音频,并将多个音频文件合并为一个。
-
视频编码参数设置:调整视频的编码参数,如比特率、分辨率、帧率等。
-
音频编码参数设置:调整音频的编码参数,如比特率、采样率、声道数等。
-
实时流媒体传输:将音视频实时传输到网络上的流媒体服务器。
-
屏幕录制:录制计算机屏幕上的活动,并将其保存为视频文件。
要使用FFmpeg,您可以通过命令行界面执行FFmpeg的命令,也可以使用FFmpeg的API在自己的应用程序中嵌入FFmpeg功能。同时,FFmpeg还有许多第三方库和工具。
解封装(本来我们看到的文件都是mp4,flv流媒体格式等格式的文件,首先需要识别这些文件格式才能解析里面的内容,这部就解封装)解码(解封装看到的数据是经过 压缩的音视频流,如果需要播放或者再次处理需要进行解码,编码的反向操作,就是视频需要显示就需要解码成显卡支持的像素格式,音频需要播放处理就需要重采样成声卡支持的格式)
常见封装格式 ; AVI 任意压缩格式,FLV,ts流媒体格式,ASF,mp4是MPEG-4压缩封装协议里面定义好的。
常用编码格式视频;H.264(存在参考帧和i帧的) ,wmv, mjpeg(每一帧都是独立的,帧内编码),帧内编码则在视频解码后播放进行拖动的时候直接播放到拖动位置的帧即可,如果是存在参考帧的在拖动的时候就需要找到后面的关键帧再解码播放的,否则不识别。
常用编码格式音频;acc(视频中一般是acc) ,MP3(早期的格式), ape ,flac(都是无损压缩的,音质很好,视频格式都是有损的压缩的)
封装格式和解码格式的整体过程
像素格式;这个需要十分关注,因为音视频方面占内存的就只有它了,在文件中是压缩好的帧格式,需要显示出来就必须解码再转换成rgb格式显示,因此内存消耗会非常大的。
YUV:YUV是一种广泛用于数字视频编码和处理的颜色空间格式。它将图像的亮度(Y)和色度(U和V)分开存储,以实现数据压缩和传输。Y表示亮度分量,而U和V表示色度分量。Y分量包含图像的黑白信息,而U和V分量包含颜色信息。
YUV格式有几种不同的变体,常见的包括YUV420、YUV422和YUV444。YUV420是最常见的格式,它将每四个像素共享一组色度分量,以实现数据的压缩。YUV422和YUV444则提供更高的色度精度,但相应地需要更多的存储空间。
在视频处理中,常常需要将图像从YUV格式转换为其他格式,例如RGB。这可以通过使用各种算法和技术来实现,例如插值、颜色空间转换等。
RGB:RGB是一种常用的颜色空间格式,它使用红(Red)、绿(Green)和蓝(Blue)三个颜色通道来表示图像的颜色。每个像素的颜色由这三个通道的数值组成,每个通道的取值范围通常是0到255。
在RGB格式中,红色的分量表示图像中的红色强度,绿色的分量表示绿色强度,蓝色的分量表示蓝色强度。通过调整这三个分量的数值,可以创建出各种不同的颜色。
与YUV格式不同,RGB格式不需要进行颜色空间转换即可直接显示在屏幕上。因此,大多数显示设备(如计算机显示器、电视等)都使用RGB格式来表示图像。
在视频处理中,常常需要将图像从YUV格式转换为RGB格式,以便在屏幕上显示。这可以通过算法和矩阵变换等技术来实现。
YUV比RGB存储的空间更小,YUV算法压缩更强,因此一般传输使用YUV格式的,而采集和显示都需要使用RGB格式的。显卡GPU比cpu计算浮点数运算是高效的。 RGB存放方面注意内存对齐的坑,一般都是采用按行读取,效率低一点,一个像素点就是一套RGB。 YUV存在的多种格式,一般会采用软解码。YUV444就跟rgb类似一个像素点就是一套YUV,YUV422就是每个像素一个Y,两个像素使用一个U,V;YUV420一般使用这种,上下左右四个像素一起,四个Y 一个u,一个v。注意还有平面和打包的两种方式,平面的又分为sp,p两种。注意在ffmpeg里面yuv平面存放格式是有三个数组来分别存放YUV的。 海思项目中介绍过的RGB,YUV
PCM音频参数 采样率 sample_rate 44100(CD) 音频频段用一个值来存放的,1秒钟采集多少次,对应到代码里面就是1s中有多少个值。 通道 channels 左右通道,双通道(数据量增大一倍),1.5通道, 样本大小(格式)就是我们采样的值是用多大来存储的,对应有一个值的。sample_size AV_SAMPLE_FMT_S16 AV_SAMPLE_FMT_FLTP 32位的一般声卡都不支持播放的,但是它是浮点数存储的,算法效率高,因此传输存储的都是这种形式,因此在播放的时候需要重采样过程将32位转换为16位。 对应音频的平面存储就是先将一个通道的所有采样值都保存再第二个通道如c1,c1,c1,c1…再c2c2c2,平面格式是对应有多个数组分别记录的,非平面的就是混合在一起的,双通道的话就是一个c1,c2,c1,c2,c1,c2。
视频帧中的GOP;就是在一段帧里面可以单独独立播放的,这一段帧就叫做GOP
二、FFmpeg整体结构
1.FFmpeg由多个组件组成,包括以下主要部分:
-
FFmpeg核心库:核心库包含了FFmpeg的主要功能,包括音视频编解码、滤镜处理、格式转换等。它是FFmpeg最重要的组件之一。
-
FFmpeg命令行工具:FFmpeg提供了一系列命令行工具,用于执行各种音视频处理任务。这些工具包括ffmpeg(用于音视频转码)、ffplay(用于播放音视频文件)、ffprobe(用于分析音视频文件)等。
-
AVCodec库:AVCodec库是FFmpeg的编解码库,它包含了各种音视频编解码器。这些编解码器可以将不同格式的音视频数据进行解码或编码。
-
AVFormat库:AVFormat库用于音视频封装和解封装,支持各种音视频容器格式(如MP4、AVI、MKV等)。它可以将音视频数据封装为特定格式,或从容器中提取音视频数据。
-
AVFilter库:AVFilter库提供了丰富的滤镜功能,用于对音视频数据进行各种处理和效果添加。可以使用滤镜库实现图像处理、颜色调整、降噪等功能。
-
SWScale库:SWScale库用于图像缩放和颜色空间转换。它可以将图像数据从一个分辨率缩放到另一个分辨率,或者将图像数据从一种颜色空间转换为另一种颜色空间。
除了上述组件,FFmpeg还包含一些其他的辅助库和工具,用于处理音视频数据。总体而言,FFmpeg是一个功能强大且灵活的开源音视频处理工具集,可以应用于各种音视频处理任务
libavutil libavutil : 包含一些公共的工具函数; AVUtil是FFmepg的核心工具库,该模块是最基础的模块之一,下面的许多其他模块都会依赖该库做一些基本的音视频处理操作。
libavformat libavformat:用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能; AVFormat 是文件格式和协议库,封装了Protocol层和Demuxer,Muxer层,使得协议和格式对于开发者来说是透明的。AVFormat中实现了目前多媒体领域中的绝大多数媒体封装格式,包括封装和解封装,如MP4, FLV, KV, TS 等文件封装格式, RTMP, RTSP, MMS, HLS 等网络协议封装格式。 FFmpeg是否支持某种媒体封装格式,取决于编译时是否包含了该格式的封装库。根据实际需求,可进行媒体封装格式的扩展,增加自己定制的封装格式,即在AVFormat中增加自己的封装处理模块。
libavcodec libavcodec:用于各种类型声音/图像编解码; AVCodec是编解码库,该模块封装了Codec层,但是有一些Codec是具备自己的License的,FFmpeg是不会默认添加像libx264,FDK-AAC,lame等库的,但是FFmpeg就像一个平台一样,可以将其他的第三方的Codec以插件的方式添加进来,然后为开发者提供统一的接口。 AVCodec中实现了目前多媒体绝大多数的编解码格式,既支持编码,也支持解码。
AVCodec除了支持MPEG4,AAC, MJPEG等自带的媒体编解码格式之外,还支持第三方的编解码器,如H.264(AVC)编码,需要使用x264编码器; H.265(HEVC)编码,需要使用x265编码器; MP3(mp3lame)编码,需要使用libmp3lame编码器。如果希望增加自己的编码格式,或者硬件编解码,则需要在AVCodec中增加相应的编解码模块。
libavfilter AVFilter : 是音视频滤镜库,该模块提供了包括音频特性和视频特效的处理,在使用FFmpeg的API进行编解码的过程中,直接使用该模块为音视频数据做特效处理时非常方便同时也非常高效的一种方式。
libavdevice AVDevice : 输入输出设备,比如,需要编译出播放声音或者视频的工具ffplay,就需要确保该模块是打开的,同时也需要libSDL的预先编译,因为该设备模块播放声音与播放视频使用的都是libSDL库。
libswscale libswscale : 用于视频场景比例缩放、色彩映射转换; SWScale 模块是将图像进行格式转换的模块,例如,可以将YUV的数据转换为RGB的数据。
libpostproc libpostproc : 用于后期效果处理; PostProc模块用来进行后期处理,当我们使用AVFilter的时候需要打开该模块的开关,因为Filter中会使用到该模块的一些基础函数。如果是比较老的FFmpeg版本,那么有可能还会编译处理avresample模块,该模块其实也是用于对音频原始数据进行重采样,但是现在已经被废弃了,不再推荐使用该库,而是使用swrresample库进行替代。
libswrressample SwrRessample 模块可用于音频重采样,可以对数字音频进行声道数,数据格式,采样率等多种基本信息的转换。
ffmpeg ffmpeg : 该项目提供的一个工具,可用于格式转换、解码或电视卡即时编码等;
ffsever ffsever : 一个 HTTP 多媒体即时广播串流服务器;
ffplay ffplay : 是一个简单的播放器,使用ffmpeg 库解析和解码,通过SDL显示;
二.ffmpeg的shell命令
一.查看FFmpeg
查看FFmpeg支持的编码器 ffmpeg configure -encoders 查看FFmpeg支持的解码器 ffmpeg configure -decoders 查看FFmpeg支持的通信协议 ffmpeg configure -protocols 查看FFmpeg所支持的音视频编码格式、文件封装格式与流媒体传输协议 ffmpeg configure --help
二.播放视频
FFmpeg ffplay input.mp4 # 播放完自动退出 ffplay -autoexit input.mp4
三.设置视频的屏幕高宽比
ffmpeg -i input.mp4 -aspect 16:9 output.mp4 通常使用的宽高比是: 16:9 4:3 16:10 5:4 2:21:1 2:35:1 2:39:1 编码格式转换 MPEG4编码转成H264编码 ffmpeg -i input.mp4 -strict -2 -vcodec h264 output.mp4 H264编码转成MPEG4编码 ffmpeg -i input.mp4 -strict -2 -vcodec mpeg4 output.mp4
四、视频压缩
ffmpeg -i 2020.mp4 -vcodec h264 -vf scale=640:-2 -threads 4 2020_conv.mp4 ffmpeg -i 1579251906.mp4 -strict -2 -vcodec h264 1579251906_output.mp4 参数解释: -i 2020.mp4 输入文件,源文件 2020_conv.mp4 输出文件,目标文件 -vf scale=640:-2 改变视频分辨率,缩放到640px宽,高度的-2是考虑到libx264要求高度是偶数,所以设置成-2,让软件自动计算得出一个接近等比例的偶数高 -threads 4 4核运算 其他参数: -s 1280x720 设置输出文件的分辨率,w*h。 -b:v 输出文件的码率,一般500k左右即可,人眼看不到明显的闪烁,这个是与视频大小最直接相关的。 -preset 指定输出的视频质量,会影响文件的生成速度,有以下几个可用的值 ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow。 与 veryslow相比,placebo以极高的编码时间为代价,只换取了大概1%的视频质量提升。这是一种收益递减准则:slow 与 medium相比提升了5%~10%;slower 与 slow相比提升了5%;veryslow 与 slower相比提升了3%。 针对特定类型的源内容(比如电影、动画等),还可以使用-tune参数进行特别的优化。 -an 去除音频流。 -vn 去除视频流。 -c:a 指定音频编码器。 -c:v 指定视频编码器,libx264,libx265,H.262,H.264,H.265。 libx264:最流行的开源 H.264 编码器。 NVENC:基于 NVIDIA GPU 的 H.264 编码器。 libx265:开源的 HEVC 编码器。 libvpx:谷歌的 VP8 和 VP9 编码器。 libaom:AV1 编码器。 -vcodec copy 表示不重新编码,在格式未改变的情况采用。 -re 以源文件固有帧率发送数据。 -minrate 964K -maxrate 3856K -bufsize 2000K 指定码率最小为964K,最大为3856K,缓冲区大小为 2000K。 -y 不经过确认,输出时直接覆盖同名文件。 -crf 参数来控制转码,取值范围为 0~51,其中0为无损模式,18~28是一个合理的范围,数值越大,画质越差。
五、视频拼接
五、视频拼接 将4个视频拼接成一个很长的视频(无声音) ffmpeg -i 0.mp4 -i 1.mp4 -i 2.mp4 -i 3.mp4 -filter_complex '[0:0][1:0] [2:0][3:0] concat=n=4:v=1 [v]' -map '[v]' output.mp4 将4个视频拼接成一个很长的视频(有声音) ffmpeg -i 1.mp4 -i 2.mp4 -i 3.mp4 -filter_complex '[0:0][0:1] [1:0][1:1] [2:0][2:1] concat=n=3:v=1:a=1 [v][a]' -map '[v]' -map '[a]’ output.mp4 参数解释: [0:0][0:1] [1:0][1:1] [2:0][2:1] 分别表示第1个输入文件的视频、音频,第2个输入文件的视频、音频,第3个输入文件的视频、音频。 concat=n=3:v=1:a=1 表示有3个输入文件,输出一条视频流和一条音频流。 [v][a] 得到的视频流和音频流的名字,注意在 bash 等 shell 中需要用引号,防止通配符扩展。 横向拼接2个视频 ffmpeg -i 0.mp4 -i 1.mp4 -filter_complex "[0:v]pad=iw*2:ih*1[a];[a][1:v]overlay=w" out.mp4 参数解释: pad 将合成的视频宽高,这里iw代表第1个视频的宽,iw*2代表合成后的视频宽度加倍,ih为第1个视频的高,合成的两个视频最好分辨率一致。 overlay 覆盖,[a][1:v]overlay=w,后面代表是覆盖位置w:0。 竖向拼接2个视频 ffmpeg -i 0.mp4 -i 1.mp4 -filter_complex "[0:v]pad=iw:ih*2[a];[a][1:v]overlay=0:h" out_2.mp4 横向拼接3个视频 ffmpeg -i 0.mp4 -i 1.mp4 -i 2.mp4 -filter_complex "[0:v]pad=iw*3:ih*1[a];[a][1:v]overlay=w[b];[b][2:v]overlay=2.0*w" out_v3.mp4 竖向拼接3个视频 ffmpeg -i 0.mp4 -i 1.mp4 -i 2.mp4 -filter_complex "[0:v]pad=iw:ih*3[a];[a][1:v]overlay=0:h[b];[b][2:v]overlay=0:2.0*h" out_v4.mp4 4个视频2x2方式排列 ffmpeg -i 0.mp4 -i 1.mp4 -i 2.mp4 -i 3.mp4 -filter_complex "[0:v]pad=iw*2:ih*2[a];[a][1:v]overlay=w[b];[b][2:v]overlay=0:h[c];[c][3:v]overlay=w:h" out.mp4
六、视频帧操作
ffmpeg和H264视频的编解码 查看每帧的信息 ffprobe -v error -show_frames gemfield.mp4 从pict_type=I可以看出这是个关键帧,然后key_frame=1 表示这是IDR frame,如果key_frame=0表示这是Non-IDR frame。 截取视频中的某一帧 把gemfield.mp4视频的第1分05秒的一帧图像截取出来。 # input seeking ffmpeg -ss 00:1:05 -i gemfield.mp4 -frames:v 1 out.jpg # output seeking ffmpeg -i gemfield.mp4 -ss 00:1:05 -frames:v 1 out1.jpg 参数解释: -frame:v 1,在video stream上截取1帧。 input seeking使用的是key frames,所以速度很快;而output seeking是逐帧decode,直到1分05秒,所以速度很慢。 重要说明: ffmpeg截取视频帧有2种 seeking 方式,对应有2种 coding 模式:transcoding 和 stream copying(ffmpeg -c copy)。 transcoding 模式:需要 decoding + encoding 的模式,即先 decoding 再encoding。 stream copying 模式:不需要decoding + encoding的模式,由命令行选项-codec加上参数copy来指定(-c:v copy )。在这种模式下,ffmpeg在video stream上就会忽略 decoding 和 encoding步骤。 查看视频总帧数 ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_frames -of default=nokey=1:noprint_wrappers=1 gemfield.mp4 查看 key frame 帧数 ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of default=nokey=1:noprint_wrappers=1 -skip_frame nokey gemfield.mp4 查看 key frame 所在的时间 ffprobe -v error -skip_frame nokey -select_streams v:0 -show_entries frame=pkt_pts_time -of csv=print_section=0 gemfield.mp4 查看 key frame 分布的情况 ffprobe -v error -show_frames gemfield.mp4 | grep pict_type 查看 key frame 所在的帧数 ffprobe -v error -select_streams v -show_frames -show_entries frame=pict_type -of csv gemfield.mp4 | grep -n I | cut -d ':' -f 1 重新设置 key frame interval ffmpeg -i gemfield.mp4 -vcodec libx264 -x264-params keyint=1:scenecut=0 -acodec copy out.mp4 查看视频波特率 ffprobe -v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 gemfield.mp4
七、图片与视频
7.1 图片转视频(规则的名称) ffmpeg -f image2 -i 'in%6d.jpg' -vcodec libx264 -r 25 -b 200k test.mp4 参数解释: -r 25 表示每秒播放25帧 -b 200k 指定码率为200k 图片的文件名为"in000000.jpg",从0开始依次递增。 7.2 图片转视频(不规则的名称) 不规则图片名称转视频。 7.2.1 方法一 不规则图片名称合成视频文件。 ffmpeg -framerate 10 -pattern_type glob -i '*.jpg' out.mp4 cat *.png | ffmpeg -f image2pipe -i - output.mp4 参数解释: -framerate 10:视频帧率 -pattern_type glob:Glob pattern 模糊匹配 -f image2pipe:图像管道,模糊匹配得到图片名称 7.2.2 方法二 不规则图片名称合成视频文件。 先动手把不规则文件重命名规则图片名。 def getTpyeFile(filelist, type): res = [] for item in filelist: name, suf = os.path.splitext(item) # 文件名,后缀 if suf == type: res.append(item) return res pwd = os.getcwd() # 返回当前目录的绝对路径 dirs = os.listdir() # 当前目录下所有的文件名组成的数组 typefiles = getTpyeFile(dirs, '.jpg') for i in range(0,len(typefiles)): os.rename(typefiles[i],"./%d.jpg" % (i)) #将文件以数字规则命令 将需要合成的图片放在txt中,通过读取txt文件合并成视频。 ffmpeg -f concat -i files.txt output.mp4 7.3 图片格式转换 ffmpeg图片格式转换 webp转换成jpg ffmpeg -i in.webp out.jpg webp转换成png ffmpeg -i in.webp out.png jpg转换成png ffmpeg -i in.jpg out.png jpg转换成webp ffmpeg -i in.jpg out.webp png转换成webp ffmpeg -i in.png out.webp png转换成jpg ffmpeg -i in.png out.jpg
官方文档
更加详细的操作请看官方文档
三、API和相关数据结构
1、ffmpeg需要注意的一些点 两种内存;因为涉及到动态链接库,则一个对象可能涉及自己本身的对象内存还有对象内部data数据指向的内存。 对象创建的方式;因为涉及对象的内存创建是在工程里面建立对象或new对象,还是声明指针通过动态链接库提供的API来申请内存初始化以及释放。(建议最好还是声明指针通过他标准的函数进行操作,在代码架构,指针只需要前置声明类名而不用知道其内部成员,以及函数操作基本都是传入指针的)
推流
拉流
3.1.1、AVFormatContext 编解封装器上下文
AVFormatContext 包含的成员主要有 { AVInputFormat iformat:输入媒体的AVInputFormat,比如指向AVInputFormat ff_flv_demuxer,就是指向具体的解封装器 unsigned int nb_streams:输入媒体的AVStream 个数 AVStream ** streams:输入媒体的AVStream []数组,一种数据流对应一个AVStream int64_t duration:输入媒体的时长(以AV_TIME_BASE为基本单位),计算方式可以参考av_dump_format()函数。 int64_t bit_rate:输入媒体的码率 }
从AVFormatContext 可以得知包含AVStream ** streams这个成员,及可以通过封装器上下文获取到av数据流信息,注意每一种数据流对应一个AVStream 的,音频流,视频流,字幕流都对应一个AVStream 。
从AVFormatContext 可以得知包含AVStream ** streams这个成员,及可以通过封装器上下文获取到av数据流信息,注意每一种数据流对应一个AVStream 的,音频流,视频流,字幕流都对应一个AVStream 。
3.1.2、AVStream AV音视频流
AVStream 包含的成员主要有 { int index:标识该视频/音频流 AVRational time_base:该流帧时间的时间基数, PTS*time_base=真正的时间(秒)注意是AVRational分数类型 AVRational avg_frame_rate: 该流的帧率 int64_t duration:该视频/音频流长度 AVCodecParameters * codecpar:编解码器参数属性,其内存申请释放与avformat_new_stream() and avformat_free_context()同步 struct AVPacketList *last_in_packet_buffer;编码是该音视频流的最后一包pck的地址 }
再可以从AVStream 成员中可以得到AVCodecParameters * codecpar:一个该音视频流对应的解码器的参数信息的变量AVCodecParameters ,但是注意这个只是流数据里面记录该流的解码器的参数信息,只是参数信息而不是解码器本身,因此我们需要先定义解码器再将其参数信息进行复制过去,从而定义出一个可以解码该音视频流的解码器。
同样解码器的信息是由一个解码器上下文进行管理的。因此要先引入解码器上下文AVCodecContext
3.1.3、AVCodecContext 解码器上下文
AVCodecContext 包含的成员主要有 { const struct AVCodec *codec:编解码器的AVCodec,比如指向AVCodec ff_aac_latm_decoder enum AVCodecID codec_id: AVRational time_base;后续帧时间都是基于这个时间基数的 int width, height:图像的宽高(只针对视频) enum AVPixelFormat pix_fmt:像素格式(只针对视频) int sample_rate:采样率(只针对音频) int channels:声道数(只针对音频) enum AVSampleFormat sample_fmt:采样格式(只针对音频) uint64_t channel_layout;通道格式(只针对音频) int thread_count;解码时使用的线程数;线程数用于决定应该将多少独立任务传递给execute() int64_t max_pixels;每幅图像最大限度接受的像素数。由用户设置 }
又得注意AVCodecContext 解码器上下文其实主要还是修饰解码器的,并且调用动态链接库的API的时候创建AVCodecContext的时候就得把AVCodec 进行创建并初始化。解码器上下文只是存储该解码器的参数信息的一个结构体,主体还是解码器AVCodec 。
又得注意AVCodecContext 解码器上下文其实主要还是修饰解码器的,并且调用动态链接库的API的时候创建AVCodecContext的时候就得把AVCodec 进行创建并初始化。解码器上下文只是存储该解码器的参数信息的一个结构体,主体还是解码器AVCodec 。
3.1.4、AVCodec 解码器上下文
AVCodec 包含的成员主要有 const char * name:编解码器名称 enum AVMediaType type:编解码器类型 enum AVCodecID id:编解码器ID • 一些编解码的接口函数,比如int (*decode)()
注意再将解码器的参数进行赋值,自己也可以进一步修改或者赋值其他
解码器参数进行赋值
avcodec_parameters_to_context(vc, acformatCon->streams[i_videoStream]->codecpar); 整体操作流程主要是这四个数据结构,但是其中还有存储数据的两个数据结构,AVPacket封装前存储的形式,AVFrame解码后用来存储的形式
整体操作流程主要是这四个数据结构,但是其中还有存储数据的两个数据结构,AVPacket封装前存储的形式,AVFrame解码后用来存储的形式
3.1.5、AVPacket封装前存储的形式
AVPacke 包含的成员主要有 { int64_t pts:显示时间戳 是以AVStream->time_base为单位的 int64_t dts:解码时间戳 是以AVStream->time_base为单位的 uint8_t * data:压缩编码数据 int size:压缩编码数据大小 int64_t pos:数据的偏移地址 int stream_index:所属的AVStream }
3.1.6、AVFrame解码后用来存储的形式
AVFrame 包含的成员主要有 { uint8_t *data[AV_NUM_DATA_POINTERS]:解码后的图像像素数据(音频采样数据) int linesize[AV_NUM_DATA_POINTERS]:对视频来说是图像中一行像素的大小;对音频来说是整个音频帧的大小, int width, height:图像的宽高(只针对视频) int key_frame:是否为关键帧(只针对视频) 。1 -> keyframe, 0-> not enum AVPictureType pict_type:帧类型(只针对视频) 。例如I, P, B int sample_rate:音频采样率(只针对音频) int nb_samples:音频每通道采样数(只针对音频) int64_t pts:显示时间戳 }
int linesize[AV_NUM_DATA_POINTERS]:为什么要存在这样的参数,如果视频的宽高不对时可能需要内存对齐,对音频来说就是对应其样本数的大小
int linesize[AV_NUM_DATA_POINTERS]:为什么要存在这样的参数,如果视频的宽高不对时可能需要内存对齐,对音频来说就是对应其样本数的大小
2.1.6、AVFrame解码后用来存储的形式 AVFrame 包含的成员主要有 { uint8_t *data[AV_NUM_DATA_POINTERS]:解码后的图像像素数据(音频采样数据) int linesize[AV_NUM_DATA_POINTERS]:对视频来说是图像中一行像素的大小;对音频来说是整个音频帧的大小, int width, height:图像的宽高(只针对视频) int key_frame:是否为关键帧(只针对视频) 。1 -> keyframe, 0-> not enum AVPictureType pict_type:帧类型(只针对视频) 。例如I, P, B int sample_rate:音频采样率(只针对音频) int nb_samples:音频每通道采样数(只针对音频) int64_t pts:显示时间戳 } 1 2 3 4 5 6 7 8 9 10 11 int linesize[AV_NUM_DATA_POINTERS]:为什么要存在这样的参数,如果视频的宽高不对时可能需要内存对齐,对音频来说就是对应其样本数的大小
3.2、解码播放相关API
3.2.1、解封装
解码步骤 包含libavformat/avformat.h和avformat.lib
与解封装相关的数据结构就有编解封装器、封装格式上下文、然后就可以根据封装格式上下文获取到压缩数据包了,再就可以进行解压产生数据帧了。 第一步就是编解封装器注册、需要要的一步,先有设备才有修饰存储设备参数的上下文。所以都得先执行下面两个函数。
av_register_all(); //注册所有解封装器 avformat_network_init();//初始化网络库,例如使用rtsp协议传输的时候就可以直接打开解封装
第二步就是获取该音视频流文件封装器的参数也就是封装格式上下文AVFormatContext,
第二步就是获取该音视频流文件封装器的参数也就是封装格式上下文AVFormatContext,
AVFormatContext *acformatCon = NULL;//注意是在动态链接库内部定义的内存 const char *strPath = "wx_camera_1615072849946.mp4"; AVDictionary *options = NULL;//通过AVDictionary 设置解封装器的参数 //注意这个参数rtsp_transport,没有在options_table.h里面,在libformat/rtsp.c文件中ff_rtsp_options, //有四个参数 udp、tcp、udp_multicast、http还可以使用+进行累加都支持的传输通道 av_dict_set(&options, "rtsp_transport", "tcp", 0);//字典设置函数 av_dict_set(&options, "max_delay", "500", 0); //调用前必须先注册解封器 通过这个函数自动将该音视频文件对应的封装器的参数存储到AVFormatContext中 int ret = avformat_open_input( &acformatCon, //分配内存空间并给参数赋值,下面的解码也是根据这些参数记录来寻找数据地址的,这里分配了内存,最后也要调用API释avformat_close_input strPath, 0,//传入0则表示自动选择解封器 &options); //不等于0则打开设置失败,并返回错误码,av_strerror函数解析错误码 if (ret != 0) { char buffer[1024]; //这个函数在avutil.lib中 ,则要添加comment这个库 av_strerror(ret, buffer, sizeof(buffer)-1);//将记录错误的返回值解析成字符串 cout << "open" << strPath << " failed :" << buffer << endl; getchar(); return -1; } cout << "open" << strPath << " success !" << endl;
其实获取完封装器上下文之后 可以进行读取流信息 第三步;读取流信息
其实获取完封装器上下文之后 可以进行读取流信息 第三步;读取流信息
avformat_find_stream_info(acformatCon, 0);//注意这一步有的其实不用调用也有数据,但是这是低频操作,调用一下也不影响效率的,只有涉及解码图像,像素这些才是高频操作,需要十分注意效率
读取之后就已经可以得到关于该音视频文件的很多参数了,但是注意,因为这只是根据封装文件的头部信息记录的数据,不一定能够全部读取出来,也就是有的部分数据在这一步可能读取不到的,还是需要到解码那里读取到。
获取音视频文件的总时间
int i_totalMs = acformatCon->duration/ (AV_TIME_BASE / 1000);//返回毫秒 cout << "i_totalMs = " << i_totalMs << endl; /* //很好的调试工具 void av_dump_format(AVFormatContext *ic, int index, //设置对应流的打印信息 const char *url, //也是打印信息 int is_output); //封装的格式是输入还是输出 这里是输入则传入0即可 */ av_dump_format(acformatCon, 0, strPath, 0);
之后就可以获取音视频流了AVStream,有两种方法,遍历和函数直接获取
//记录stream的音视频索引,方便在读取是区分音视频 int i_videoStream = 0; int i_audioStream = 1; //获取音视频流的信息 (遍历判断stream流数组哪个是a哪个是v,或者使用函数调用) //一般streams[0]是视频 1是音频,但是没有规定,因此还是需要判断 for (int i = 0; i < acformatCon->nb_streams; i++) { i_audioStream = i; AVStream *as = acformatCon->streams[i]; //音视频共用的 cout << "format :" << as->codecpar->format << endl;//样本格式,这个format 会根据音视频转换,视频为AVPixelFormat,音频为AVSampleFormat cout << "codec_id :" << as->codecpar->codec_id << endl;//对应的是枚举的AVCodecID 之后解码是需要用到的 //注意formate类型的数据在解码出来之后不能直接播放的,因为是32位的,计算机声卡一般都不支持32位的,需要重采样到16位或其他才能播放 //判断音频 if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)//宏在avutil.h里面定义的 { cout << i << ":音频信息" << endl; cout << "sample_rate :" << as->codecpar->sample_rate << endl;//音频的采样率 cout << "channels :" << as->codecpar->channels << endl; //音频的一帧数据代表单通道一定量的样本数,保证一帧数据的数据量,不多也不少,定量 cout << "audio fbs = " << r2b(as->avg_frame_rate) << endl;//音频却不一定是整数,表示单通道多少个样本数 cout << "audio frame_size = " << as->codecpar->frame_size << endl;//记录一帧的数据量,同样也是以编码那边为准的 //audio fbs = sample_rate/ frame_size; 存在这样一个等式的,注意音频和视频的帧率是不一样的,音视频是分开做缓存的 } //判断视频 else if (as->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { i_videoStream = i; cout << i << ":视频信息" << endl; cout << "width :" << as->codecpar->width << endl; cout << "height :" << as->codecpar->height << endl;//在有的流媒体数据在打开解封装这一步,是还不能解析出来宽高的,因此写程序宽高不能写死从这里获取,但是在解码的肯定可以获取到的 //帧率,是一个AVRational有理数也就是分数类型,保证精确的,视频的fbs是整数的表示多少张图片,但音频却不一定,表示单通道多少个样本数 //分数需要转换的,但是注意分母为0的情况 cout << "vedio fbs = " << r2b(as->avg_frame_rate) << endl; } //还有可能是字幕 AVMEDIA_TYPE_SUBTITLE,暂不处理 } //第二种方法,直接使用函数获取音视频流信息 /* int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type,//获取流的类型 int wanted_stream_nb,//自己想要流的编号 -1则表示自动选择 int related_stream,//相关的流信息,null表示没有 AVCodec **decoder_ret,//对应解码要用到的,但是这里也不用,因为解封装和解码新版本已经分割开来了。 //如果在这里去获取解码器,则把解码和解封装混到一起了,与新版本不一致,我们是可以根据codeID自己找到的,没必要在这里设置 int flags); */ //直接返回对应流的索引,再根据stream就可以直接访问了 i_videoStream = av_find_best_stream(acformatCon, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); i_audioStream = av_find_best_stream(acformatCon, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
3.2.2、解码 解码步骤 包含include/avcodec.h和lib/avcodec.lib
第一步;首先同样要注册解码器,才能使用解码器和解码器上下文
//注册解码器 avcodec_register_all();
第二步;再注意因为前面就根据音视频分出了不同的数据流AVStream,所以解码器也是分开的,因此要分别对音频和视频进行解码,则创建解码器和解码器上下文也是需要两份的。
第二步;再注意因为前面就根据音视频分出了不同的数据流AVStream,所以解码器也是分开的,因此要分别对音频和视频进行解码,则创建解码器和解码器上下文也是需要两份的。
//视频解码器 //找到解码器(可以通过av_find_best_stream来但是把解封装和解码合到一起了不好,可以通过codecid获取) AVCodec *vcodec = avcodec_find_decoder(acformatCon->streams[i_videoStream]->codecpar->codec_id); if (!vcodec) { cout << "can't find the codec id :" << acformatCon->streams[i_videoStream]->codecpar->codec_id; getchar(); return -1; } cout << "find the codec id :" << acformatCon->streams[i_videoStream]->codecpar->codec_id << endl; //创建找到解码器上下文 AVCodecContext *vc = avcodec_alloc_context3(vcodec); //配置解码器参数 //复制的方法 avcodec_parameters_to_context(vc, acformatCon->streams[i_videoStream]->codecpar); //还可以自己进行解码器参数, //设置8进程解码,也可以通过api获取得到系统进程数 vc->thread_count = 8; //打开解码器上下文 ret = avcodec_open2(vc, NULL, 0); if (ret != 0) { char buffer[1024]; //这个函数在avutil.lib中 ,则要添加comment这个库 av_strerror(ret, buffer, sizeof(buffer) - 1);//将记录错误的返回值解析成字符串 cout << "avcodec_open2 failed! : " << buffer << endl; getchar(); return -1; } cout << "vedio avcodec_open2 success!" << endl;
//音频解码器 //找到解码器(可以通过av_find_best_stream来但是把解封装和解码合到一起了不好,可以通过codecid获取) AVCodec *acodec = avcodec_find_decoder(acformatCon->streams[i_audioStream]->codecpar->codec_id); if (!acodec) { cout << "can't find the codec id :" << acformatCon->streams[i_audioStream]->codecpar->codec_id; getchar(); return -1; } cout << "find the codec id :" << acformatCon->streams[i_audioStream]->codecpar->codec_id << endl; //创建找到解码器上下文 AVCodecContext *ac = avcodec_alloc_context3(acodec); //配置解码器参数 //复制的方法 avcodec_parameters_to_context(ac, acformatCon->streams[i_audioStream]->codecpar); //设置8进程解码,也可以通过api获取得到系统进程数 ac->thread_count = 8; //打开解码器上下文 ret = avcodec_open2(ac, NULL, 0); if (ret != 0) { char buffer[1024]; //这个函数在avutil.lib中 ,则要添加comment这个库 av_strerror(ret, buffer, sizeof(buffer) - 1);//将记录错误的返回值解析成字符串 cout << "avcodec_open2 failed! : " << buffer << endl; getchar(); return -1; } cout << "audio avcodec_open2 success!" << endl;
第三步;获取了视频流/音频流的解码器以及解码器参数的上下文后我们就可以从数据流中取包然后解码成帧了。
主要步骤就是
av_packet_alloc、av_frame_alloc调用接口定义AVPacket 和AVFrame , for (; ;)循环读取解压 av_read_frame读取包AVPacket avcodec_send_packet将读取到的包AVPacket 发送到对应解码器的解码线程 av_packet_unref 因为AVPacket 变量重复使用,但是其data数据区是用完一次就可以释放的,调用unref进行清空,引用计数-1. for循环 循环读取解压过来的帧。 avcodec_receive_frame读取解压过来返回的帧,注意解压一包可能返回多帧因此需要for循环来循环读取解压过来的帧。 最后再释放内存 av_frame_free(&frame); av_packet_free(&pkt);//释放要传入地址
详细代码如下;
//读取视频帧 //调用接口申请内存比较方便,因为后续维护pkt需要放到队列里面,用指针比较好,比较指针可以转为void类型 //并且在调用接口处也方便,只需要类的前置声明即可,不需要全部声明出来,架构更加清晰 AVPacket *pkt = av_packet_alloc();//调用接口申请空间,那么就要调用接口释放 AVFrame *frame = av_frame_alloc(); for (; ;) { //int av_read_frame(AVFormatContext *s, AVPacket *pkt); int re = av_read_frame(acformatCon, pkt); if (re != 0)//失败的或者读到文件结尾 { //文件读取结束后又移动到对应位置循环播放 cout << "--------------end---------------" << endl; int ms = 3000;//三秒的位置,再根据时间基数(分数)转换 long long pos = (double)ms / (double)1000 * r2b(acformatCon->streams[pkt->stream_index]->time_base); av_seek_frame(acformatCon, i_videoStream,pos, AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME);//往后对齐,对齐到关键帧 //break;//空间不需要释放的,因为内部data空间没有成功 continue; } cout << "packet size = " << pkt->size << endl; //显示时间 cout << "packet pts(time_base) = " << pkt->pts<< endl; //编码时间 cout << "packet dts(time_base) = " << pkt->dts << endl; //音视频的time_base是可能不一样的,因此要做成相同单位方便做同步 cout << "packet pts(ms) = " << pkt->pts * r2b(acformatCon->streams[pkt->stream_index]->time_base) *1000;//这里的pts,dts都是以这个时间为基数的也是一个分数 //负数就是表示预编码 AVCodecContext *cc = NULL; if (pkt->stream_index == i_videoStream) { cout << "picture" << endl; cc = vc; } else if (pkt->stream_index == i_audioStream) { cout << "audio" << endl; cc = ac; } //解码视频 因为音视频涉及相同的接口 ret = avcodec_send_packet(cc, pkt);//发送packet到解码线程 //XSleep(500);//500ms //pkt有内部的空间,已经上个pkt已经发到解码线程了则可以释放其内部data //但其pkt这个变量还是可以使用的,则使用内存引用计数-1的方法,为0则释放空间 av_packet_unref(pkt); if (ret != 0) { char buffer[1024]; //这个函数在avutil.lib中 ,则要添加comment这个库 av_strerror(ret, buffer, sizeof(buffer) - 1);//将记录错误的返回值解析成字符串 cout << "avcodec_open2 failed! : " << buffer << endl; continue; } //接收,这里要注意发送是不占用cpu时间的采用多线程的方法 //发1可以会接收多个,因此需要循环的,并且最后需要传入null来把后面的缓冲帧都接收 for (; ; ) { re = avcodec_receive_frame(cc, frame); if (re != 0) break; cout << "recv frame :" << frame->format << " " << frame->linesize[0] << endl; } } av_frame_free(&frame); av_packet_free(&pkt);//释放要传入地址
3.2.3、视频像素和尺寸转换 swscale
包含的头文件libswscale/swscale.h和库"swscale.lib"
注意这一部分使用显卡来做效率更高,但是ffmpeg也提供了这类型的库给我们,ffmpeg使用简单,但是性能开销还是比较大的,因为视频像素涉及的内存空间太大了,一张图片这么多个像素点一个像素点有需要几个字节来存储,因此视频像素的尺寸转换是十分影响效率的。
转换格式参数上下文的创建和释放 获取它的两个API函数,区别就是第一个参数区别, 第一个sws_getContext,就是直接创建一个新的上下文空间给你, 第二个sws_getCachedContext,就是你可以把之前创建好的上下文传入进去,他会缓冲里面去找输入输出的格式是否一样,因为都是一套设定的,如果存在这一套机制,那么就会返回相同的指针,来给你使用,因此第一次是可以传入NULL的,但是如果之后再次传入SwsContext ,并且与之前的不一致的话,那么他就会把之前的SwsContext 空间释放掉重新分配返回回来的,因此需要注意多线程同时访问的时候会存在问题,要上锁的,所以在多线程当中最好还是独立建一个SwsContext ,进行使用。
struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat, int dstW, int dstH, enum AVPixelFormat dstFormat, int flags, SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param); struct SwsContext *sws_getCachedContext(struct SwsContext *context, int srcW, int srcH, enum AVPixelFormat srcFormat, int dstW, int dstH, enum AVPixelFormat dstFormat, int flags, SwsFilter *srcFilter SwsFilter *dstFilter, const double *param); int srcW, int srcH, int dstW, int dstH, 原的宽高,目的宽高 enum AVPixelFormat srcFormat, dstFormat 原像素格式,目的像素格式 int flags SWScale库本身提供了很多套算法,flags就是选择对应的尺寸转换算法,不同的算法会有不同的性能和尺寸转换后的差异。 SwsFilter *srcFilter 过滤器暂时不考虑,直接设置null即可 double *param 是与flags设置的算法传参有关的,可以不用管直接使用默认的即可
释放视频转换上下文空间,但是注意这个只是传入指针,因此要注意释放后这个变量指针没有置0,需要后面手动置0,因为上面有创建函数是直接判断是否为空进行处理的,如果这里释放了内存不置空那边调用就会崩溃的。 void sws_freeContext(struct SwsContext *swsContext);
3.2.4、具体尺寸格式转换的函数 sws_scale
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[], const int srcStride[], int srcSliceY, int srcSliceH, uint8_t *const dst[], const int dstStride[]); const uint8_t *const srcSlice[];原数据存放的地址,一个数组至于取几个要看前面像素格式决定的,如果是YUV平面格式的就要取3位分别对应YUV,如果是RGB打包格式的那么就只取一位。 const int srcStride[], 一行字节数的大小,就是之前frame里面的linesize。 int srcSliceY 不用的 int srcSliceH 高度,不用宽度,因为使用的是内存对齐后的linesize了。 uint8_t *const dst[];转换后的目的数据存放的空间,是一个指针数组,因此要提前分配空间, const int dstStride[] 输出的一行的大小
swscale实践
swscale实践
//像素格式和尺寸转换上下文 SwsContext *vctx = NULL; unsigned char *rgb = NULL; //在解码后得到解码帧后进行转换的,注意内存的申请和格式转换 //视频 if (cc == vc) { vctx = sws_getCachedContext( vctx, //传NULL会新创建 frame->width, frame->height, //输入的宽高 (AVPixelFormat)frame->format, //输入格式 YUV420p frame->width, frame->height, //输出的宽高 AV_PIX_FMT_RGBA, //输入格式RGBA SWS_BILINEAR, //尺寸变化的算法 0, 0, 0); //if(vctx) //cout << "像素格式尺寸转换上下文创建或者获取成功!" << endl; //else // cout << "像素格式尺寸转换上下文创建或者获取失败!" << endl; if (vctx) { //申请转换后数据存放的内存 if (!rgb) rgb = new unsigned char[frame->width*frame->height * 4]; //转换为对应存放格式,因为是要转换为打包的RGBA格式因此只需要一维,如果是YUV420P的则需要三维都需要内存地址的,否则会奔溃 uint8_t *data[2] = { 0 }; data[0] = rgb; int lines[2] = { 0 }; lines[0] = frame->width * 4;//存放一行数据大小, re = sws_scale(vctx, frame->data, //输入数据 frame->linesize, //输入行大小 0, frame->height, //输入高度 data, //输出数据和大小 lines //一行的大小 ); cout << "sws_scale = " << re << endl; } }
四.小网站
📖 Documentation: ffmpeg Documentation
📖 Wiki: FFmpeg
📖 IRC: #ffmpeg
✉ Mailing list: ffmpeg-user Info Page
🌐 Stack Overflow: Stack Overflow - Where Developers Learn, Share, & Build Careers and use #ffmpeg
🌐 Super User: Super User and use #ffmpeg
以上文章借鉴 卖酒的小码农,花花少年。