ffmpeg介绍
1 ffserver命令
fserver是一个音频和视频的流式服务器。它通过在启动时读入的配置文件完成配置,不指定时用默认的/etc/ffserver.conf文件。ffserver接受一些或者FFM流作为输入然后通过RTP/RTSP/HTTP推流。ffserver监听在配置文件中指定的端口,在配置文件中输入的流叫做feed,每一个都是用<Feed>的节来指定的。每一个feed可以有不同格式的的不同输出流,每一个在配置文件中用<Stream>节来指定。ffserver是通过推送用ffmpeg编码的流来工作的。ffserver担当一个HTTPserver的角色,接收来自ffmpeg的获得发布流的POST请求,并且用流媒体内容来服务HTTP/RTSP客户端的GET请求。要搞清楚什么是feed,什么是stream。
1.1 Feed是什么
feed是由ffmpeg创建的FFM流,并且发送到ffserver正在监听的端口上。每一个feed是通过一个唯一的名字来识别的,这个名字关联到发布在ffserver上的资源的名字,并且是通过配置文件里面的<Feed>节来完成配置的。feed发布的URL是以下面的形式给出的:
http://<ffserver_ip_address>:<http_port>/<feed_name>
其中,ffserver_ip_address是ffserver安装的主机的IP地址,http_port是HTTP服务器的端口号,feed_name是相关的定义在配置文件中的feed的名字。每一个feed跟一个磁盘上的文件相关联,这个文件用于当新内容被实时地加入进流的时候,这个文件是用于允许发送预先录制的数据尽可能快地到一个播放器。
1.2 stream是什么
一个实时流或者一个流是一个由ffserver发布的资源并且通过HTTP协议使客户可以访问。一个流可以连接到一个feed上,或者一个文件上。当连接到一个feed上的时候,发布的流是推的来自相关联的通过运行的ffmpeg的实例所形成的feed上的。在第二种情况中,流是从pre-recorded文件里读来的。每一个流有一个唯一的名字,关联到ffserver上的资源的名字,并且是通过配置文件中的复杂的stream节来配置的。访问流的HTTP地址形式:
http://<ffserver_ip_address>:<http_port>/<stream_name>[<options>]
访问流的RTSP地址形式如下:
http://<ffserver_ip_address>:<http_port>/<stream_name>[<options>]
其中,stream_name是配置文件中定义的流的名字,options是在URL后面指定的选项的列表,将影响流如何通过ffserver提供的方式。
2 ffmpeg命令
ffmpeg是一个可以从现场的音视频源中采集的非常快速的视频和音频转换器。可以在任意的采样率之间转换,并且可以在现场resize视频。ffmpeg用-i参数从任意数量的输入files中读入,并写入任意数量的输出文件(通过普通的输出文件名指定)。从输入中选择哪个流到输出流是自动完成或者用-map选项指定。为了引用选项中的输入文件,必须用索引号。类似地,文件里的流也是通过索引号引用的。如2:3引用第3个输入文件中的第4个流。一般的规则是选项是用在下一个指定的文件上的,因此,顺序是很重要的。每出现一个将运用到下一个输入或者输出文件上。这个规则的例外是最开始就指定的全局选项。
不要混淆输入和输出文件,首先指定完所有的输入文件,然后才指定输出文件。也不要混淆属于不同的文件的选项,所有选项仅仅运用在下一个输入或输出文件上,并且在不同的文件之间会重置。
下面来3个实例:
a 设置输出文件的视频码率为64kbps ffmpeg -i input.avi -b:v 64k -bufsize 64k output.avi b 强制输出文件的帧率为24fps: ffmpeg -i input.avi -r 24 output.avi c 强制输入文件的帧率为1fps并且输出文件的帧率为24fps ffmpeg -r 1 -i input.avi -r 24 output.avi
原始输入文件需要格式化选项。
ffmpeg调用libavformat(包含解封装)读入输入文件并从中取得包含了编码数据的包。当有多个输入文件,ffmpeg通过跟踪最低的时间戳或者跟踪任何激活的输入流来保持同步。编码的包然后传递给解码器(除非指定是拷贝流,那么就不经过解码以及后面的编码)。解码器产生可以被filtering进一步处理的非压缩帧,在filtering之后,帧传给encoder(编码他们并且输出编码包),最后,传给封装器muxer,把编码包写给输出文件。在编码之前ffmpeg可以libavfilter库中的滤波器来处理原始音视频帧。滤波器链产生了滤波器图,ffmpeg有两种类型的滤波器图:简单的和复杂的。
flv格式详解
FLV(Flash Video)是现在非常流行的流媒体格式,由于其视频文件体积轻巧、封装播放简单等特点,使其很适合在网络上进行应用,目前主流的视频网站无一例外地使用了FLV格式。另外由于当前浏览器与Flash Player紧密的结合,使得网页播放FLV视频轻而易举,也是FLV流行的原因之一。
FLV是流媒体封装格式,我们可以将其数据看为二进制字节流。总体上看,FLV包括文件头(File Header)和文件体(File Body)两部分,其中文件体由一系列的Tag及Tag Size对组成。
FLV格式解析
先来一张图,这是《东风破》——周杰伦(下载)的一个MV视频。我使用的是Binary Viewer的二进制查看工具。
header
头部分由一下几部分组成
Signature(3 Byte)+Version(1 Byte)+Flags(1 Bypte)+DataOffset(4 Byte)
- signature 占3个字节
固定FLV三个字符作为标示。一般发现前三个字符为FLV时就认为他是flv文件。 - Version 占1个字节
标示FLV的版本号。这里我们看到是1 - Flags 占1个字节
内容标示。第0位和第2位,分别表示 video 与 audio 存在的情况.(1表示存在,0表示不存在)。截图看到是0x05
,也就是00000101
,代表既有视频,也有音频。 - DataOffset 4个字节
表示FLV的header长度。这里可以看到固定是9
body
FLV的body部分是由一系列的back-pointers + tag构成
- back-pointers 固定4个字节,表示前一个tag的size。
- tag 分三种类型,video、audio、scripts。
tag组成
tag type
+tag data size
+Timestamp
+TimestampExtended
+stream id
+ tag data
- type 1个字节。8为Audio,9为Video,18为scripts
- tag data size 3个字节。表示tag data的长度。从streamd id 后算起。
- Timestreamp 3个字节。时间戳
- TimestampExtended 1个字节。时间戳扩展字段
- stream id 3个字节。总是0
- tag data 数据部分
我们根据实例来分析:
看到第一个TAG
type=0x12
=18。这里应该是一个scripts。
size=0x000125
=293。长度为293。
timestreamp=0x000000
。这里是scripts,所以为0
TimestampExtended =0x00
。
stream id =0x000000
我们看一下TAG的data部分:
tag的划分
图中红色部分是我标出的两个back-pointers,都是4个字节。而中间就是第一个TAG。那是怎么计算的呢?我们就以这个做个示例。
- 首先第一个back-pointers是
0x00000000
,那是因为后面是第一个TAG。所以他为0。 - 然后根据我们我们前面格式获取到size是
0x000125
。也就是说从stream id后面再加上293个字节就到了第一个TAG的末尾,我们数一下一下。stream id以前总共有24个字节(9+4+11)。那么到第一个TAG结束,下一个TAG开始的位置是293+24=137=0x13D
。 - 接下来我们找到
0x13D
的地址,从工具上很容易找到,正好就是红色下划线的前面。红色部分是0x00000130
=304,这代表的是上一个TAG的大小。 - 最后我们计算一下,上一个TAG数据部分是293个字节,前面type、stream id等字段占了11个字节。正好是匹配的。
上面我们已经知道了怎么取划分每个TAG。接下来我们就看TAG的具体内容
tag的内容
前面已经提到tag分3种。我们一个个看
script
脚本Tag一般只有一个,是flv的第一个Tag,用于存放flv的信息,比如duration、audiodatarate、creator、width等。
首先介绍下脚本的数据类型。所有数据都是以数据类型+(数据长度)+数据的格式出现的,数据类型占1byte,数据长度看数据类型是否存在,后面才是数据。
一般来说,该Tag Data结构包含两个AMF包。AMF(Action Message Format)是Adobe设计的一种通用数据封装格式,在Adobe的很多产品中应用,简单来说,AMF将不同类型的数据用统一的格式来描述。第一个AMF包封装字符串类型数据,用来装入一个“onMetaData”标志,这个标志与Adobe的一些API调用有,在此不细述。第二个AMF包封装一个数组类型,这个数组中包含了音视频信息项的名称和值。具体说明如下,大家可以参照图片上的数据进行理解。
值 | 类型 | 说明 |
---|---|---|
0 | Number type | 8 Bypte Double |
1 | Boolean type | 1 Bypte bool |
2 | String type | 后面2个字节为长度 |
3 | Object type | |
4 | MovieClip type | |
5 | Null type | |
6 | Undefined type | |
7 | Reference type | |
8 | ECMA array type | 数组,类似Map |
10 | Strict array type | |
11 | Date type | |
12 | Long string type | 后面4个字节为长度 |
上图为第一个AMF包
- type=
0x02
对应String - size=
0A
=10 -
value=onMetaData 正好是10个字节。
-
上图为第二个AMF
- type=
0x08
对应ECMA array type。
表示数组,类似Map。后面4个字节为数组的个数。然后是键值对,第一个为键,2个字节为长度。后面跟具体的内容。接着3个字节表示值的类型,然后根据类型判断长度。
上图我们可以判断,总共有13个键值对。
第一个长度为8个字节是duration。值类型是0x004073
,第一个字节是00,所以是double,8个字节。
第二个长度5个字节是width。值也是double类型,8个字节。
依次解析下去...
到处,我们已经知道了如何解析FLV中Tag为script的数据。
video
type=0x09
=9。这里应该是一个video。
size=0x000030
=48。长度为48。
timestreamp=0x000000
。
TimestampExtended =0x00
。
stream id =0x000000
我们看到数据部分:
视频信息+数据
视频信息,1个字节。
前4位为帧类型Frame Type
值 | 类型 |
---|---|
1 | keyframe (for AVC, a seekable frame) 关键帧 |
2 | inter frame (for AVC, a non-seekable frame) |
3 | disposable inter frame (H.263 only) |
4 | generated keyframe (reserved for server use only) |
5 | video info/command frame |
后4位为编码ID (CodecID)
值 | 类型 |
---|---|
1 | JPEG (currently unused) |
2 | Sorenson H.263 |
3 | Screen video |
4 | On2 VP6 |
5 | On2 VP6 with alpha channel |
6 | Screen video version 2 |
7 | AVC |
特殊情况
视频的格式(CodecID)是AVC(H.264)的话,VideoTagHeader会多出4个字节的信息,AVCPacketType 和CompositionTime。
- AVCPacketType 占1个字节
值 | 类型 |
---|---|
0 | AVCDecoderConfigurationRecord(AVC sequence header) |
1 | AVC NALU |
2 | AVC end of sequence (lower level NALU sequence ender is not required or supported) |
AVCDecoderConfigurationRecord.包含着是H.264解码相关比较重要的sps和pps信息,再给AVC解码器送数据流之前一定要把sps和pps信息送出,否则的话解码器不能正常解码。而且在解码器stop之后再次start之前,如seek、快进快退状态切换等,都需要重新送一遍sps和pps的信息.AVCDecoderConfigurationRecord在FLV文件中一般情况也是出现1次,也就是第一个video tag.
- CompositionTime 占3个字节
条件 | 值 |
---|---|
AVCPacketType ==1 | Composition time offset |
AVCPacketType !=1 | 0 |
我们看第一个video tag,也就是前面那张图。我们看到AVCPacketType =0。而后面三个字节也是0。说明这个tag记录的是AVCDecoderConfigurationRecord。包含sps和pps数据。
再看到第二个video tag
我们看到 AVCPacketType =1,而后面三个字节为000043
。这是一个视频帧数据。
解析到的数据完全符合上面的理论。
sps pps
前面我们提到第一个video 一般存放的是sps和pps。这里我们具体解析下sps和pps内容。先看下存储的格式(图6):
0x01
+sps[1]
+sps[2]
+sps[3]
+0xFF
+0xE1
+sps size
+sps
+01
+pps size
+pps
我们看到图 。
sps[1]=0x64
sps[2]=00
sps[3]=0D
sps size=0x001B
=27
跳过27个字节后,是0x01
pps size=0x0005
=5
跳过5个字节,就到了back-pointers。
视频帧数据
解析出sps和pps tag后,后面的video tag就是真正的视频数据内容了
这是第二个video tag其实和图8一样,只是我圈出来关键信息。先看下格式
frametype=0x17
=00010111
AVCPacketType =1
Composition Time=0x000043
后面就是NALU DATA
Audio
与视频格式类似
前4位为音频格式
值 | 类型 |
---|---|
0 | Linear PCM, platform endian |
1 | ADPCM |
2 | MP3 |
3 | Linear PCM, little endian |
4 | Nellymoser 16-kHz mono |
5 | Nellymoser 8-kHz mono |
6 | Nellymoser |
7 | G.711 A-law logarithmic PCM |
8 | G.711 mu-law logarithmic PCM |
9 | reserved |
10 | AAC |
11 | Speex |
14 | MP3 8-Khz |
15 | Device-specific sound |
接着2位为采样率
值 | 类型 |
---|---|
0 | 5.5-kHz |
1 | 11-kHz |
2 | 22-kHz |
3 | 44-kHz |
对于AAC总是3
接着1位为采样的长度
值 | 类型 |
---|---|
0 | snd8Bit |
1 | snd16Bit |
压缩过的音频都是16bit
接着1位为音频类型
值 | 类型 |
---|---|
0 | sndMono |
1 | sndStereo |
对于AAC总是1
我们看到第三个TAG
ffmpeg播放器基本原理
其中,源文件模块是这个播放器的起始,主要是为下面的各个模块以数据包的方式提供数据流。具体而言就是从本地视频文件中读取出数据包,然后将其按照一定的顺序排列,源源不断得发送到下面的解复用模块中。
解复用模块根据源文件的容器格式来分离出视频流、音频流和字幕流,在加入时间同步等信息后传送给下面的解码模块。为识别出不同的文件类型和媒体类型,常规的做法是读取一部分数据,然后遍历解复用播放器支持的文件格式和媒体数据格式,做匹配来确定是哪种文件类型,哪种媒体类型,有些媒体类型的原始数据外面还有其他的信息,比如时间,包大小,是否完整包等等。这里要注意的是,时钟信息的计算工作也是在这个模块完成,用于各媒体之间的同步。
解码模块作用就是解码数据包,并且把同步时钟信息传递下去。对视频媒体而言,通常是解码成YUV 数据,然后利用显卡硬件直接支持YUV 格式数据Overlay 快速显示的特性让显卡极速显示。YUV格式是一个统称,常见的有YV12,YUY2,UYVY 等等。有些非常古老的显卡和嵌入式系统不支持YUV 数据显示,那就要转换成RGB 格式的数据,每一帧的每一个像素点都要转换,分别计算RGB 分量,并且因为转换是浮点运算,虽然有定点算法,还是要耗掉相当一部分CPU,总体上效率底下;对音频媒体而言,通常是解码成PCM 数据,然后送给声卡直接输出。
颜色空间转换模块的作用是把视频解码器解码出来的数据转换成当前显示系统支持的颜色格式。通常视频解码器解码出来的是YUV 数据,PC 系统是直接支持YUV 格式的,也支持RGB 格式,有些嵌入式系统只支持RGB 格式的。
渲染模块对视频来说就是显示视频图像,对音频来说就是播放声音,对字幕来说就是显示字幕,并保持视频、音频和字幕的同步播放。
ffmpeg转码基本原理
转码器在视音频编解码处理的程序中,属于一个比较复杂的东西。因为它结合了视频的解码和编码。一个视频播放器,一般只包含解码功能;一个视频编码工具,一般只包含编码功能;而一个视频转码器,则需要先对视频进行解码,然后再对视频进行编码,因而相当于解码器和编码器的结合。下图例举了一个视频的转码流程。输入视频的封装格式是FLV,视频编码标准是H.264,音频编码标准是AAC;输出视频的封装格式是AVI,视频编码标准是MPEG2,音频编码标准是MP3。从流程中可以看出,首先从输入视频中分离出视频码流和音频压缩码流,然后分别将视频码流和音频码流进行解码,获取到非压缩的像素数据/音频采样数据,接着将非压缩的像素数据/音频采样数据重新进行编码,获得重新编码后的视频码流和音频码流,最后将视频码流和音频码流重新封装成一个文件。
Linux下ndk编译移植FFmpeg到Android平台:https://github.com/EricLi22/AndroidMultiMedia
FFmpeg 推流手机摄像头,实现直播-:https://github.com/979451341/RtmpCamera
Android中使用ffmpeg编码进行rtmp推流- https://www.jianshu.com/p/f3a55d3d1f5d
ffmpeg编码RiemannLeeLiveProject- https://github.com/liweiping1314521/RiemannLeeLiveProject
Android利用ffmpeg推流-:https://github.com/WritingMinds/ffmpeg-android-java
https://blog.****.net/xiejiashu/article/details/74783875
https://blog.****.net/rainweic/article/details/94666527
https://blog.****.net/helloxiaoliang/article/details/81020482
https://my.oschina.net/u/1983790/blog/490524
https://blog.****.net/qq_26464039/article/details/84503335
https://www.cnblogs.com/zhangwc/p/9817642.html
https://blog.****.net/leixiaohua1020/article/details/12751349
https://www.jianshu.com/p/d541b317f71c
https://www.cnblogs.com/groundsong/p/5146112.html
https://www.cnblogs.com/lidabo/p/8662955.html
https://blog.****.net/jgw2008/article/details/84954902
https://www.cnblogs.com/lcxiao/p/11509132.html
ffprobe的常用命令:
https://www.jianshu.com/p/e14bc2551cfd
https://juejin.im/post/5a59993cf265da3e4f0a1e4b
import picamera import time import traceback import ctypes from librtmp import * global meta_packet global start_time class Writer(): # camera可以通过一个类文件的对象来输出,实现write方法即可 conn = None # rtmp连接 sps = None # 记录sps帧,发过以后就不需要再发了(抓包看到ffmpeg是这样的) pps = None # 同上 sps_len = 0 # 同上 pps_len = 0 # 同上 time_stamp = 0 def __init__(self, conn): self.conn = conn def write(self, data): try: # 寻找h264帧间隔符 indexs = [] index = 0 data_len = len(data) while index < data_len - 3: if ord(data[index]) == 0x00 and ord(data[index + 1]) == 0x00 and ord( data[index + 2]) == 0x00 and ord(data[index + 3]) == 0x01: indexs.append(index) index = index + 3 index = index + 1 # 寻找h264帧间隔符 完成 # 通过间隔符个数确定类型,树莓派摄像头的第一帧是sps+pps同时发的 if len(indexs) == 1: # 非sps pps帧 buf = data[4: len(data)] # 裁掉原来的头(00 00 00 01),把帧内容拿出来 buf_len = len(buf) type = ord(buf[0]) & 0x1f if type == 0x05: # 关键帧,根据wire shark抓包结果,需要拼装sps pps 帧内容 三部分,长度都用4个字节表示 body0 = 0x17 data_body_array = [bytes(bytearray( [body0, 0x01, 0x00, 0x00, 0x00, (self.sps_len >> 24) & 0xff, (self.sps_len >> 16) & 0xff, (self.sps_len >> 8) & 0xff, self.sps_len & 0xff])), self.sps, bytes(bytearray( [(self.pps_len >> 24) & 0xff, (self.pps_len >> 16) & 0xff, (self.pps_len >> 8) & 0xff, self.pps_len & 0xff])), self.pps, bytes(bytearray( [(buf_len >> 24) & 0xff, (buf_len >> 16) & 0xff, (buf_len >> 8) & 0xff, (buf_len) & 0xff])), buf ] mbody = \'\'.join(data_body_array) time_stamp = 0 # 第一次发出的时候,发时间戳0,此后发真时间戳 if self.time_stamp != 0: time_stamp = int((time.time() - start_time) * 1000) packet_body = RTMPPacket(type=PACKET_TYPE_VIDEO, format=PACKET_SIZE_LARGE, channel=0x06, timestamp=time_stamp, body=mbody) packet_body.packet.m_nInfoField2 = 1 else: # 非关键帧 body0 = 0x27 data_body_array = [bytes(bytearray( [body0, 0x01, 0x00, 0x00, 0x00, (buf_len >> 24) & 0xff, (buf_len >> 16) & 0xff, (buf_len >> 8) & 0xff, (buf_len) & 0xff])), buf] mbody = \'\'.join(data_body_array) # if (self.time_stamp == 0): self.time_stamp = int((time.time() - start_time) * 1000) packet_body = RTMPPacket(type=PACKET_TYPE_VIDEO, format=PACKET_SIZE_MEDIUM, channel=0x06, timestamp=self.time_stamp, body=mbody) self.conn.send_packet(packet_body) elif len(indexs) == 2: # sps pps帧 if self.sps is not None: return data_body_array = [bytes(bytearray([0x17, 0x00, 0x00, 0x00, 0x00, 0x01]))] sps = data[indexs[0] + 4: indexs[1]] sps_len = len(sps) pps = data[indexs[1] + 4: len(data)] pps_len = len(pps) self.sps = sps self.sps_len = sps_len self.pps = pps self.pps_len = pps_len data_body_array.append(sps[1:4]) data_body_array.append(bytes(bytearray([0xff, 0xe1, (sps_len >> 8) & 0xff, sps_len & 0xff]))) data_body_array.append(sps) data_body_array.append(bytes(bytearray([0x01, (pps_len >> 8) & 0xff, pps_len & 0xff]))) data_body_array.append(pps) data_body = \'\'.join(data_body_array) body_packet = RTMPPacket(type=PACKET_TYPE_VIDEO, format=PACKET_SIZE_LARGE, channel=0x06, timestamp=0, body=data_body) body_packet.packet.m_nInfoField2 = 1 self.conn.send_packet(meta_packet, queue=True) self.conn.send_packet(body_packet, queue=True) except Exception, e: traceback.print_exc() def flush(self): pass def get_property_string(string): # 返回两字节string长度及string length = len(string) return \'\'.join([chr((length >> 8) & 0xff), chr(length & 0xff), string]) def get_meta_string(string): # 按照meta packet要求格式返回bytes,带02前缀 return \'\'.join([chr(0x02), get_property_string(string)]) def get_meta_double(db): nums = [0x00] fp = ctypes.pointer(ctypes.c_double(db)) cp = ctypes.cast(fp, ctypes.POINTER(ctypes.c_longlong)) for i in range(7, -1, -1): nums.append((cp.contents.value >> (i * 8)) & 0xff) return \'\'.join(bytes(bytearray(nums))) def get_meta_boolean(isTrue): nums = [0x01] if (isTrue): nums.append(0x01) else: nums.append(0x00) return \'\'.join(bytes(bytearray(nums))) conn = RTMP( \'rtmp://192.168.199.154/oflaDemo/test\', # 推流地址 live=True) librtmp.RTMP_EnableWrite(conn.rtmp) conn.connect() start_time = time.time() # 拼装视频格式的数据包 meta_body_array = [get_meta_string(\'@setDataFrame\'), get_meta_string(\'onMetaData\'), bytes(bytearray([0x08, 0x00, 0x00, 0x00, 0x06])), # 两个字符串和ECMA array头,共计6个元素,注释掉了音频相关数据 get_property_string(\'width\'), get_meta_double(640.0), get_property_string(\'height\'), get_meta_double(480.0), get_property_string(\'videodatarate\'), get_meta_double(0.0), get_property_string(\'framerate\'), get_meta_double(25.0), get_property_string(\'videocodecid\'), get_meta_double(7.0), # get_property_string(\'audiodatarate\'), get_meta_double(125.0), # get_property_string(\'audiosamplerate\'), get_meta_double(44100.0), # get_property_string(\'audiosamplesize\'), get_meta_double(16.0), # get_property_string(\'stereo\'), get_meta_boolean(True), # get_property_string(\'audiocodecid\'), get_meta_double(10.0), get_property_string(\'encoder\'), get_meta_string(\'Lavf57.56.101\'), bytes(bytearray([0x00, 0x00, 0x09])) ] meta_body = \'\'.join(meta_body_array) print meta_body.encode(\'hex\') meta_packet = RTMPPacket(type=PACKET_TYPE_INFO, format=PACKET_SIZE_LARGE, channel=0x04, timestamp=0, body=meta_body) meta_packet.packet.m_nInfoField2 = 1 # 修改stream id stream = conn.create_stream(writeable=True) with picamera.PiCamera() as camera: camera.start_preview() time.sleep(2) camera.start_recording(Writer(conn), format=\'h264\', resize=(640, 480), intra_period=25, quality=25) # 开始录制,数据输出到Writer的对象里 while True:#永远不停止 time.sleep(60) camera.stop_recording() camera.stop_preview()