道底wwW117vv如何vv7溜坡能解决吗,是否117vvCoM能再恢复

814204 条评论分享收藏感谢收起
https://www.zhihu.com/video/098112
赞同 407407 条评论分享收藏感谢收起&如何实现1080P延迟低于500ms的实时超清直播传输技术& &关于直播,所有的技术细节都在这里了&
导语:视频直播是很多技术团队及架构师关注的问题,在实时性方面,大部分直播是准实时的,存在 1-3 秒延迟。本文作者袁荣喜,介绍其将直播延迟控制在 500ms 的背后的实现。
袁荣喜,学霸君工程师,2015 年加入学霸君,负责学霸君的网络实时传输和分布式系统的架构设计和实现,专注于基础技术领域,在网络传输、数据库内核、分布式系统和并发编程方面有一定了解。
最近由于公司业务关系,需要一个在公网上能实时互动超清视频的架构和技术方案。众所周知,视频直播用 CDN + RTMP 就可以满足绝大部分视频直播业务,我们也接触了和测试了几家 CDN 提供的方案,单人直播没有问题,一旦涉及到多人互动延迟非常大,无法进行正常的互动交谈。对于我们做在线教育的企业来说没有互动的直播是毫无意义的,所以我们决定自己来构建一个超清晰(1080P)实时视频的传输方案。
先来解释下什么是实时视频,实时视频就是视频图像从产生到消费完成整个过程人感觉不到延迟,只要符合这个要求的视频业务都可以称为实时视频。关于视频的实时性归纳为三个等级:
市面上大部分真实时视频都是 480P 或者 480P 以下的实时传输方案,用于在线教育和线上教学有一定困难,而且有时候流畅度是个很大的问题。在实现超清晰实时视频我们做了大量尝试性的研究和探索,在这里会把大部分细节分享出来。
要实时就要缩短延迟,要缩短延迟就要知道延迟是怎么产生的,视频从产生、编码、传输到最后播放消费,各个环节都会产生延迟,总体归纳为下图:
成像延迟,一般的技术是毫无为力的,涉及到 CCD 相关的硬件,现在市面上最好的&CCD,一秒钟 50 帧,成像延迟也在 20 毫秒左右,一般的&CCD&只有 20 ~ 25 帧左右,成像延迟 40 ~ 50 毫秒。
编码延迟,和编码器有关系,在接下来的小结介绍,一般优化的空间比较小。
我们着重针对网络延迟和播放缓冲延迟来进行设计,在介绍整个技术细节之前先来了解下视频编码和网络传输相关的知识和特点。
一、视频编码那些事
我们知道从&CCD&采集到的图像格式一般的 RGB 格式的(BMP),这种格式的存储空间非常大,它是用三个字节描述一个像素的颜色值,如果是 1080P 分辨率的图像空间:1920 x 1080 x 3 = 6MB,就算转换成 JPG 也有近 200KB,如果是每秒 12 帧用 JPG 也需要近 2.4MB/S 的带宽,这带宽在公网上传输是无法接受的。
视频编码器就是为了解决这个问题的,它会根据前后图像的变化做运动检测,通过各种压缩把变化的发送到对方,1080P 进行过 &H.264&编码后带宽也就在 200KB/S ~ 300KB/S 左右。在我们的技术方案里面我们采用 H.264 作为默认编码器(也在研究 H.265)。
1.1 H.264 编码
前面提到视频编码器会根据图像的前后变化进行选择性压缩,因为刚开始接收端是没有收到任何图像,那么编码器在开始压缩的视频时需要做个全量压缩,这个全量压缩在 H.264 中 I 帧,后面的视频图像根据这个I帧来做增量压缩,这些增量压缩帧叫做 P 帧,H.264 为了防止丢包和减小带宽还引入一种双向预测编码的 B 帧,B 帧以前面的 I 或 P 帧和后面的 P 帧为参考帧。H.264 为了防止中间 P 帧丢失视频图像会一直错误它引入分组序列(GOP)编码,也就是隔一段时间发一个全量 I 帧,上一个 I 帧与下一个 I
帧之间为一个分组 GOP。它们之间的关系如下图:
PS:在实时视频当中最好不要加入 B 帧,因为 B 帧是双向预测,需要根据后面的视频帧来编码,这会增大编解码延迟。
1.2 马赛克、卡顿和秒开
前面提到如果 GOP 分组中的P帧丢失会造成解码端的图像发生错误,其实这个错误表现出来的就是马赛克。因为中间连续的运动信息丢失了,H.264 在解码的时候会根据前面的参考帧来补齐,但是补齐的并不是真正的运动变化后的数据,这样就会出现颜色色差的问题,这就是所谓的马赛克现象,如图:
这种现象不是我们想看到的。为了避免这类问题的发生,一般如果发现 P 帧或者 I 帧丢失,就不显示本 GOP 内的所有帧,直到下一个 I 帧来后重新刷新图像。但是 I 帧是按照帧周期来的,需要一个比较长的时间周期,如果在下一个 I 帧来之前不显示后来的图像,那么视频就静止不动了,这就是出现了所谓的卡顿现象。如果连续丢失的视频帧太多造成解码器无帧可解,也会造成严重的卡顿现象。视频解码端的卡顿现象和马赛克现象都是因为丢帧引起的,最好的办法就是让帧尽量不丢。
知道 H.264 的原理和分组编码技术后所谓的秒开技术就比较简单了,只要发送方从最近一个 GOP 的 I 帧开发发送给接收方,接收方就可以正常解码完成的图像并立即显示。但这会在视频连接开始的时候多发一些帧数据造成播放延迟,只要在接收端播放的时候尽量让过期的帧数据只解码不显示,直到当前视频帧在播放时间范围之内即可。
1.3 编码延迟与码率
前面四个延迟里面我们提到了编码延迟,编码延迟就是从&CCD&出来的 RGB 数据经过 H.264 编码器编码后出来的帧数据过程的时间。我们在一个 8 核 CPU 的普通客户机测试了最新版本 X.264 的各个分辨率的延迟,数据如下:
二、网络传输质量因素
实时互动视频一个关键的环节就是网络传输技术,不管是早期 VoIP,还是现阶段流行的视频直播,其主要手段是通过 TCP/IP 协议来进行通信。但是 IP 网络本来就是不可靠的传输网络,在这样的网络传输视频很容易造成卡顿现象和延迟。先来看看 IP 网络传输的几个影响网络传输质量关键因素。
2.1 TCP 和 UDP
对直播有过了解的人都会认为做视频传输首选的就是 TCP + RTMP,其实这是比较片面的。在大规模实时多媒体传输网络中,TCP 和 RTMP 都不占优势。TCP 是个拥塞公平传输的协议,它的拥塞控制都是为了保证网络的公平性而不是快速到达,我们知道,TCP 层只有顺序到对应的报文才会提示应用层读数据,如果中间有报文乱序或者丢包都会在&TCP&做等待,所以&TCP&的发送窗口缓冲和重发机制在网络不稳定的情况下会造成延迟不可控,而且传输链路层级越多延迟会越大。
关于&TCP&的原理:
关于&TCP&重发延迟:
http://weibo.com/p/
在实时传输中使用 UDP 更加合理,UDP 避免了&TCP&繁重的三次握手、四次挥手和各种繁杂的传输特性,只需要在 UDP 上做一层简单的链路 QoS 监测和报文重发机制,实时性会比&TCP&好,这一点从 RTP 和 DDCP 协议可以证明这一点,我们正式参考了这两个协议来设计自己的通信协议。
要评估一个网络通信质量的好坏和延迟一个重要的因素就是 Round-Trip Time(网络往返延迟),也就是 RTT。评估两端之间的 RTT 方法很简单,大致如下:
示意图如下:
上面步骤的探测周期可以设为 1 秒一次。为了防止网络突发延迟增大,我们采用了借鉴了&TCP&的 RTT 遗忘衰减的算法来计算,假设原来的 RTT 值为 rtt,本次探测的 RTT 值为 keep_rtt。那么新的 RTT 为:
new_rtt = (7 * rtt + keep_rtt) / 8
可能每次探测出来的 keep_rtt 会不一样,我们需要会计算一个 RTT 的修正值 rtt_var,算法如下:
new_rtt_var = (rtt_var * 3 + abs(rtt – keep_rtt)) / 4&
rtt_var 其实就是网络抖动的时间差值。
如果 RTT 太大,表示网络延迟很大。我们在端到端之间的网络路径同时保持多条并且实时探测其网络状态,如果 RTT 超出延迟范围会进行传输路径切换(本地网络拥塞除外)。
2.3 抖动和乱序
UDP 除了延迟外,还会出现网络抖动。什么是抖动呢?举个例子,假如我们每秒发送 10 帧视频帧,发送方与接收方的延迟为 50MS,每帧数据用一个 UDP 报文来承载,那么发送方发送数据的频率是 100ms 一个数据报文,表示第一个报文发送时刻 0ms, T2 表示第二个报文发送时刻 100ms . . .,如果是理想状态下接收方接收到的报文的时刻依次是(50ms, 150ms, 250ms, 350ms….),但由于传输的原因接收方收到的报文的相对时刻可能是(50ms, 120ms, 240ms, 360ms
….),接收方实际接收报文的时刻和理想状态时刻的差值就是抖动。如下示意图:
我们知道视频必须按照严格是时间戳来播放,否则的就会出现视频动作加快或者放慢的现象,如果我们按照接收到视频数据就立即播放,那么这种加快和放慢的现象会非常频繁和明显。也就是说网络抖动会严重影响视频播放的质量,一般为了解决这个问题会设计一个视频播放缓冲区,通过缓冲接收到的视频帧,再按视频帧内部的时间戳来播放既可以了。
UDP 除了小范围的抖动以外,还是出现大范围的乱序现象,就是后发的报文先于先发的报文到达接收方。乱序会造成视频帧顺序错乱,一般解决的这个问题会在视频播放缓冲区里做一个先后排序功能让先发送的报文先进行播放。
播放缓冲区的设计非常讲究,如果缓冲过多帧数据会造成不必要的延迟,如果缓冲帧数据过少,会因为抖动和乱序问题造成播放无数据可以播的情况发生,会引起一定程度的卡顿。关于播放缓冲区内部的设计细节我们在后面的小节中详细介绍。
UDP 在传输过程还会出现丢包,丢失的原因有多种,例如:网络出口不足、中间网络路由拥堵、socket 收发缓冲区太小、硬件问题、传输损耗问题等等。在基于 UDP 视频传输过程中,丢包是非常频繁发生的事情,丢包会造成视频解码器丢帧,从而引起视频播放卡顿。这也是大部分视频直播用&TCP&和&RTMP&的原因,因为&TCP&底层有自己的重传机制,可以保证在网络正常的情况下视频在传输过程不丢。基于 UDP 丢包补偿方式一般有以下几种:
报文冗余很好理解,就是一个报文在发送的时候发送 2 次或者多次。这个做的好处是简单而且延迟小,坏处就是需要额外 N 倍(N 取决于发送的次数)的带宽。
Forward Error Correction,即向前纠错算法,常用的算法有纠删码技术(EC),在分布式存储系统中比较常见。最简单的就是 A B 两个报文进行 XOR(与或操作)得到 C,同时把这三个报文发往接收端,如果接收端只收到 AC,通过 A 和 C 的 XOR 操作就可以得到 B 操作。这种方法相对增加的额外带宽比较小,也能防止一定的丢包,延迟也比较小,通常用于实时语音传输上。对于 &KB/S 码率的超清晰视频,哪怕是增加 20% 的额外带宽都是不可接受的,所以视频传输不太建议采用
FEC 机制。
丢包重传有两种方式,一种是 push 方式,一种是 pull 方式。Push 方式是发送方没有收到接收方的收包确认进行周期性重传,TCP 用的是 push 方式。pull 方式是接收方发现报文丢失后发送一个重传请求给发送方,让发送方重传丢失的报文。丢包重传是按需重传,比较适合视频传输的应用场景,不会增加太对额外的带宽,但一旦丢包会引来至少一个 RTT 的延迟。
2.5 MTU 和最大 UDP
IP 网定义单个 IP 报文最大的大小,常用 MTU 情况如下:
超通道 65535
16Mb/s 令牌环 179144
Mb/s 令牌环 4464
以太网 1500
IEEE 802.3/802.2 1492
点对点(低时延)296
红色的是 Internet 使用的上网方式,其中 X.25 是个比较老的上网方式,主要是利用 ISDN 或者电话线上网的设备,也不排除有些家用路由器沿用 X.25 标准来设计。所以我们必须清晰知道每个用户端的 MTU 多大,简单的办法就是在初始化阶段用各种大小的&UDP&报文来探测 MTU 的大小。MTU 的大小会影响到我们视频帧分片的大小,视频帧分片的大小其实就是单个&UDP&报文最大承载的数据大小。
分片大小 = MTU – IP 头大小 – UDP 头大小 – 协议头大小;
IP 头大小 = 20 字节, UDP 头大小 = 8 字节。
为了适应网络路由器小包优先的特性,我们如果得到的分片大小超过 800 时,会直接默认成 800 大小的分片。
三、传输模型
我们根据视频编码和网络传输得到特性对 1080P 超清视频的实时传输设计了一个自己的传输模型,这个模型包括一个根据网络状态自动码率的编解码器对象、一个网络发送模块、一个网络接收模块和一个&UDP&可靠到达的协议模型。各个模块的关系示意图如下:
3.1 通信协议
先来看通信协议,我们定义的通信协议分为三个阶段:接入协商阶段、传输阶段、断开阶段。
接入协商阶段:
主要是发送端发起一个视频传输接入请求,携带本地的视频的当前状态、起始帧序号、时间戳和&MTU&大小等,接收方在收到这个请求后,根据请求中视频信息初始化本地的接收通道,并对本地&MTU&和发送端&MTU&进行比较取两者中较小的回送给发送方, 让发送方按协商后的&MTU&来分片。示意图如下:
传输阶段:
传输阶段有几个协议,一个测试量 RTT 的 PING/PONG 协议、携带视频帧分片的数据协议、数据反馈协议和发送端同步纠正协议。其中数据反馈协议是由接收反馈给发送方的,携带接收方已经接收到连续帧的报文&ID、帧&ID&和请求重传的报文&ID&序列。同步纠正协议是由发送端主动丢弃发送窗口缓冲区中的报文后要求接收方同步到当前发送窗口位置,防止在发送主动丢弃帧数据后接收方一直要求发送方重发丢弃的数据。示意图如下:
断开阶段:
就一个断开请求和一个断开确认,发送方和接收方都可以发起断开请求。
发送主要包括视频帧分片算法、发送窗口缓冲区、拥塞判断算法、过期帧丢弃算法和重传。先一个个来介绍。
前面我们提到&MTU&和视频帧大小,在 1080P 下大部分视频帧的大小都大于 UDP 的 MTU 大小,那么就需要对帧进行分片,分片的方法很简单,按照先连接过程协商后的 MTU 大小来确定分片大小(确定分片大小的算法在&MTU&小节已经介绍过),然后将 帧数据按照分片大小切分成若干份,每一份分片以 segment 报文形式发往接收方。
重传比较简单,我们采用 pull 方式来实现重传,当接收方发生丢包,如果丢包的时刻 T1 + rtt_var& 接收方当前的时刻 T2,就认为是丢包了,这个时候就会把所有满足这个条件丢失的报文&ID&构建一个 segment ack 反馈给发送方,发送方收到这个反馈根据&ID&到重发窗口缓冲区中查找对应的报文重发即可。
为什么要间隔一个 rtt_var 才认为是丢包了?因为报文是有可能乱序到达,所有要等待一个抖动周期后认为丢失的报文还没有来才确认是报文丢失了,如果检测到丢包立即发送反馈要求重传,有可能会让发送端多发数据,造成带宽让费和网络拥塞。
发送窗口缓冲区
发送窗口缓冲区保存这所有正在发送且没有得到发送方连续&ID&确认的报文。当接收方反馈最新的连续报文&ID,发送窗口缓冲就会删除所有小于最新反馈连续的报文&ID,发送窗口缓冲区缓冲的报文都是为了重发而存在。这里解释下接收方反馈的连续的报文 ID,举个例子,假如发送方发送了 1. 2. 3. 4. 5,接收方收到 1.2. 4. 5。这个时候最小连续&ID&= 2,如果后面又来了 3,那么接收方最小连续
我们把当前时间戳记为 curr_T,把发送窗口缓冲区中最老的报文的时间戳记为 oldest_T,它们之间的间隔记为 delay,那么
delay = curr_T – oldest_T
在编码器请求发送模块发送新的视频帧时,如果 delay & 拥塞阈值 Tn,我们就认为网络拥塞了,这个时候会根据最近 20 秒接收端确认收到的数据大小计算一个带宽值,并把这个带宽值反馈给编码器,编码器收到反馈后,会根据带宽调整编码码率。如果多次发生要求降低码率的反馈,我们会缩小图像的分辨率来保证视频的流畅性和实时性。Tn 的值可以通过 rtt 和 rtt_var 来确定。
但是网络可能阶段性拥塞,过后却恢复正常,我们设计了一个定时器来定时检查发送方的重发报文数量和 delay,如果发现恢复正常,会逐步增大编码器编码码率,让视频恢复到指定的分辨率和清晰度。
过期帧丢弃
在网络拥塞时可能发送窗口缓冲区中有很多报文正在发送,为了缓解拥塞和减少延迟我们会对整个缓冲区做检查,如果有超过一定阈值时间的 H.264 GOP 分组存在,我们会将这个 GOP 所有帧的报文从窗口缓冲区移除。并将它下一个 GOP 分组的 I 的帧&ID&和报文&ID&通过 wnd sync 协议同步到接收端上,接收端接收到这个协议,会将最新连续&ID&设置成同步过来的 ID。这里必须要说明的是如果频繁出现过期帧丢弃的动作会造成卡顿,说明当前网络不适合传输高分辨率视频,可以直接将视频设成更小的分辨率
接收主要包括丢包管理、播放缓冲区、缓冲时间评估和播放控制,都是围绕播放缓冲区来实现的,一个个来介绍。
丢包管理包括丢包检测和丢失报文&ID&管理两部分。丢包检测过程大致是这样的,假设播放缓冲区的最大报文&ID&为 max_id,网络上新收到的报文&ID&为 new_id,如果 max_id + 1 & new_id,那么可能发生丢包,就会将 [max_id + 1, new_id -1] 区间中所有的 ID 和当前时刻作为 K/V 对加入到丢包管理器当中。如果 new_id & max_id,那么就将丢包管理中的 new_id 对应的 K/V 对删除,表示丢失的报文已经收到。当收包反馈条件满足时,会扫描整个丢包管理,将达到请求重传的丢包&ID&加入到
segment ack 反馈消息中并发往发送方请求重传,如果&ID&被请求了重传,会将当前时刻设置为 K/V 对中,增加对应报文的重传计数器 count,这个扫描过程会统计对包管理器中单个重发最多报文的重发次数 resend_count。
缓冲时间评估
在前面的抖动与乱序小节中我们提到播放端有个缓冲区,这个缓冲区过大时延迟就大,缓冲区过小时又会出现卡顿现象,我们针对这个问题设计了一个缓冲时间评估的算法。缓冲区评估先会算出一个 cache timer,cache timer 是通过扫描对包管理得到的 resend count 和 rtt 得到的,我们知道从请求重传报文到接收方收到重传的报文的时间间隔是一个 RTT 周期,所以 cache timer 的计算方式如下。
cache timer = (2 * resend_count+ 1) * (rtt + rtt_var) / 2
有可能 cache timer 计算出来很小(小于视频帧之间间隔时间 frame timer),那么 cache timer = frame timer,也就是说网络再好,缓冲区缓冲区至少 1 帧视频的数据,否则缓冲区是毫无意义的。
如果单位时间内没有丢包重传发生,那么 cache timer 会做适当的缩小,这样做的好处是当网络间歇性波动造成 cache timer 很大,恢复正常后 cache timer 也能恢复到相对小位置,缩减不必要的缓冲区延迟。
播放缓冲区
我们设计的播放缓冲区是按帧&ID&为索引的有序循环数组,数组内部的单元是视频帧的具体信息:帧&ID、分片数、帧类型等。缓冲区有两个状态:waiting 和 playing,waiting 状态表示缓冲区处于缓冲状态,不能进行视频播放直到缓冲区中的帧数据达到一定的阈值。Playing 状态表示缓冲区进入播放状态,播放模块可以从中取出帧进行解码播放。我们来介绍下这两个状态的切换关系:
播放缓冲区的目的就是防止抖动和应对丢包重传,让视频流能按照采集时的频率进行播放,播放缓冲区的设计极其复杂,需要考虑的因素很多,实现的时候需要慎重。
接收端最后一个环节就是播放控制,播放控制就是从缓冲区中拿出有效的视频帧进行解码播放。但是怎么拿?什么时候拿?我们知道视频是按照视频帧从发送端携带过来的相对时间戳来做播放,我们每一帧视频都有一个相对时间戳 TS,根据帧与帧之间的 TS 的差值就可以知道上一帧和下一帧播放的时间间隔,假如上一帧播放的绝对时间戳为 prev_play_ts,相对时间戳为 prev_ts,当前系统时间戳为 curr_play_ts,当前缓冲区中最小序号帧的相对时间戳为 &frame_ts,只要满足:
Prev_play_ts + (frame_ts – prev_ts) & curr_play_ts&且这一帧数据是所有的报文都收齐了
这两个条件就可以进行解码播放,取出帧数据后将 Prev_play_ts = cur_play_ts,但更新 prev_ts 有些讲究,为了防止缓冲延迟问题我们做了特殊处理。
如果 frame_ts + cache timer & 缓冲区中最大帧的 ts,表明缓冲的时延太长,则 prev_ts = 缓冲区中最大帧的 ts – cache timer。 否则 prev_ts = frame_ts。
再好的模型也需要有合理的测量方式来验证,在多媒体这种具有时效性的传输领域尤其如此。一般在实验室环境我们采用 netem 来进行模拟公网的各种情况进行测试,如果在模拟环境已经达到一个比较理想的状态后会组织相关人员在公网上进行测试。下面来介绍怎么来测试我们整个传输模型的。
4.1 netem 模拟测试
Netem 是 Linux 内核提供的一个网络模拟工具,可以设置延迟、丢包、抖动、乱序和包损坏等,基本能模拟公网大部分网络情况。
关于 netem 可以访问它的官网:
https://wiki.linuxfoundation.org/networking/netem
我们在实验环境搭建了一个基于服务器和客户端模式的测试环境,下面是测试环境的拓扑关系图:
我们利用 Linux 来做一个路由器,服务器和收发端都连接到这个路由器上,服务器负责客户端的登记、数据转发、数据缓冲等,相当于一个简易的流媒体服务器。Sender 负责媒体编码和发送,receiver 负责接收和媒体播放。为了测试延迟,我们把 sender 和 receiver 运行在同一个 PC 机器上,在 sender 从&CCD&获取到 RGB 图像时打一个时间戳,并把这个时间戳记录在这一帧数据的报文发往 server 和 receiver,receiver 收到并解码显示这帧数据时,通过记录的时间戳可以得到整个过程的延迟。我们的测试用例是用
1080P 码率为 300KB/S 视频流,在 router 用 netem 上模拟了以下几种网络状态:
因为传输机制采用的是可靠到达,那么检验传输机制有效的参数就是视频延迟,我们统计 2 分钟周期内最大延迟,以下是各种情况的延迟曲线图:
从上图可以看出,如果网络控制在环路延迟在 200ms 丢包在 10% 以下,可以让视频延迟在 500ms 毫秒以下,这并不是一个对网络质量要求很苛刻的条件。所以我们在后台的媒体服务部署时,尽量让客户端到媒体服务器之间的网络满足这个条件,如果网路环路延迟在 300ms 丢包 15% 时,依然可以做到小于 1 秒的延迟,基本能满足双向互动交流。
4.2 公网测试
公网测试相对比较简单,我们将 Server 部署到 UCloud 云上,发送端用的是上海电信 100M 公司宽带,接收端用的是河北联通 20M 小区宽带,环路延迟在 60ms 左右。总体测试下来 1080P 在接收端观看视频流畅自然,无抖动,无卡顿,延迟统计平均在 180ms 左右。
在整个 1080P 超清视频的传输技术实现过程中,我们遇到过比较多的坑。大致如下:
Socket 缓冲区问题
我们前期开发阶段都是使用 socket 默认的缓冲区大小,由于 1080P 图像帧的数据非常巨大(关键帧超过 80KB),我们发现在在内网测试没有设置丢包的网络环境发现接收端有严重的丢包,经查证是 socket 收发缓冲区太小造成丢包的,后来我们把 socket 缓冲区设置到 128KB 大小,问题解决了。
H.264 B 帧延迟问题
前期我们为了节省传输带宽和防丢包开了 B 帧编码,由于 B 帧是前后双向预测编码的,会在编码期滞后几个帧间隔时间,引起了超过 100ms 的编码延时,后来我们为了实时性干脆把 B 帧编码选项去掉。
Push 方式丢包重传
在设计阶段我们曾经使用发送端主动 push 方式来解决丢包重传问题,在测试过程发现在丢包频繁发生的情况下至少增加了 20% 的带宽消耗,而且容易带来延迟和网络拥塞。后来几经论证用现在的 pull 模式来进行丢包重传。
Segment 内存问题
在设计阶段我们对每个视频缓冲区中的帧信息都是动态分配内存对象的,由于 1080P 在传输过程中每秒会发送 400 – 500 个 UDP 报文,在 PC 端长时间运行容易出现内存碎片,在服务器端出现莫名其妙的 clib 假内存泄露和并发问题。我们实现了一个 memory slab 管理频繁申请和释放内存的问题。
音频和视频数据传输问题
在早期的设计之中我们借鉴了 FLV 的方式将音频和视频数据用同一套传输算法传输,好处就是容易实现,但在网络波动的情况下容易引起声音卡顿,也无法根据音频的特性优化传输。后来我们把音频独立出来,针对音频的特性设计了一套低延迟高质量的音频传输体系,定点对音频进行传输优化。
后续的工作是重点放在媒体器多点分布、多点并发传输、P2P 分发算法的探索上,尽量减少延迟和服务带宽成本,让传输变的更高效和更低廉。
提问:在优化到 500ms 方案中,哪一块是最关键的?
袁荣喜:主要是丢包重传 拥塞和播放缓冲这三者之间的协调工作最为关键,要兼顾延迟控制和视频流畅性。
提问:多方视频和单方有哪些区别,用到了 CDN 推流吗?
袁荣喜:我们公司是做在线教育的,很多场景需要老师和学生交谈,用 CDN 推流方式延迟很大,我们这个视频主要是解决多方通信之间交谈延迟的问题。我们现在观看放也有用 CDN 推流,但只是单纯的观看。我们也在研发基于 UDP 的观看端分发协议,目前这部分工作还没有完成。
http://www.yunweipai.com/archives/9037.html
关于直播,所有的技术细节都在这里了
(可以从网上找原始文章,这里简单转载)
网络视频直播存在已有很长一段时间,随着移动上下行带宽提升及资费的下调,视频直播被赋予了更多娱乐和社交的属性,人们享受随时随地进行直播和观看,主播不满足于单向的直播,观众则更渴望互动,直播的打开时间和延迟变成了影响产品功能发展重要指标。那么,问题来了:&如何实现低延迟、秒开的直播?
先来看看视频直播的5个关键的流程:录制-&编码-&网络传输-&解码-&播放,每个环节对于直播的延迟都会产生不同程度的影响。这里重点分析移动设备的情况。受限于技术的成熟度、硬件环境等,我们针对移动场景简单总结出直播延迟优化的4个点:网络、协议、编解码、移动终端,并将分四大块来一一解密UCloud直播云实现低延迟、秒开的技术细节。
一、UCloud直播云实现接入网络优化的技术细节:
1)全局负载均衡-就近接入
实现就近接入的技术比较广为人知,就是CDN即Content Delivery Network (内容分发网络)。CDN包含两大核心技术:负载均衡和分发网络,随着10多年的演进,对负载均衡和分发的实现方式已多种多样,分发网络的构建策略通常是经过日积月累的总结出一套最合适的分发路由,并且也不是一成不变,需时刻关注调整,动态运营。这里重点介绍下CDN的负载均衡技术。
负载均衡是如何实现让用户就进访问的呢?比较普遍的实现方式:通过用户使用的DNS服务器来判断客户端所在的网络位置,从而返回对应的服务IP。如下图示例:
广东电信用户IP:1.1.1.1 需要看一个直播http://www.ucloud.cn/helloworld.flv&,实现就近访问的过程是:
1&用户向配置的DNS服务器1.1.1.0(通常是运营商指定,也称local DNS,后面简称Ldns)发起www.ucloud.cn&的查询;
2& Ldns 上没有该域名的记录,则往顶级即Root NS上发起查询;
3&Root NS返回告知Ldns该域名的权威解析记录在UCloud NS上;
4&Ldns 向UCloud NS发起查询;
5&UCloud NS 向UCloud GSLB服务发起查询,GSLB发现 Ldns1.1.1.0是属于广东电信;
6&返回广东电信的就近节节点IP1.1.1.2;
7&返回1.1.1.2给Ldns;
8&返回给用户1.1.1.2,用户到1.1.1.2上去获取直播内容。
链路很长,但是每个Ldns上都会对查询过的域名做合理的缓存,下一个广东电信的用户再来查询的时候就可以直接返回1.1.1.2。架构并不复杂,关键点是如何知道Ldns是位于广东电信,这就涉及一个IP地址库。有开源地址库,也有商业地址库,可以按需求采购即可,一般一年1万左右。这里不难看出来,调度的准确度是完全依赖用户配置的Ldns,而这些Ldns大多数是省级别的,即GLSB只知道用户是广东电信,但是常常分不出来是广东广州电信,还是广东深圳电信。
HTTPDNS就是实现更精准的调度一种方式:
1&用户1.1.1.1通过HTTP协议直接向UCloud NS请求直播域名www.ucloud.cn;
2&UCloud NS发现用户IP1.1.1.1属于广东深圳电信;
3&返回广东深圳电信节点1.1.1.11给UCloud NS;
4&返回给用户。
HTTPDNS的好处显而易见:一可精准获得用户端的IP,有效避免用户配错Ldns(有时是网络中心配错DNS)的情况,可更精准定位用户所在网络位置。二可避免DNS解析劫持。
2)BGP中转架构-最短传输路径
BGP即Border Gateway Protocol (边界网关协议),业内简称BGP。为什么BGP中转架构对直播加速和分发如此重要?不得不提国内复杂的网络状况,较广为人知的是“南电信北联通”的宽带用户分布。那一个简单的问题,电信主播发起了直播,联通的用户想看怎么办呢? 从结构上讲,肯定是有有限个电信联通两个运营商的交汇点,相当于信息桥梁。 这就会带来两个问题:1、路程要绕远,网络延迟高且不稳定;2、高峰期拥堵,导致直播流卡顿。
BGP的技术原理往简单的说就是允许同一IP在不同网络中广播不同的路由信息,效果就是同一个IP,当电信用户来访问时走电信网内的路由,联通用户来访问时走的联通的路由。所以BGP技术对跨运营商的访问带来了巨大的便利,特别是直播场景。不同于传统的文件缓存场景,一个图片哪怕第一次是跨了遥远的距离从源站获取后,本地网络进行缓存,后面的访问都走本地网络。直播加速是流式的,并且当要做到低延迟的时候,中间的缓存要尽可能少。 BGP相当于给跨网的用户就近搭建了一坐桥梁,不必绕远路,延时和稳定性都大大提高了。
技术原理部分介绍完了,那么多直播延迟影响有多少改善呢?首先这里的就近,不一定是物理距离近,不考虑瞬时负载情况下,更多是指测速延时最优的机房。在国内一般而言相同的接入运营商(电信、联通、移动)并且地理位置最近的情况网络延迟最优,小于15ms。跨省同运营商的网络延迟25~50ms,跨运营商情况更复杂一些,在50~100ms。总结起来,直播当中每个包的延时可以缩短100ms,由于网络的叠加效果,反射到上层是秒级的延迟缩减。
二、直播应用层协议及传输层协议的选择以及对直播体验影响的分析&
直播协议的选择
国内常见公开的直播协议有几个:RTMP、HLS、HDL(HTTP-FLV)、RTP,我们来逐一介绍。
RTMP协议:
是Adobe的专利协议,现在大部分国外的CDN已不支持。在国内流行度很高。原因有几个方面:
1、开源软件和开源库的支持稳定完整。如斗鱼主播常用的OBS软件,开源的librtmp库,服务端有nginx-rtmp插件。
2、播放端安装率高。只要浏览器支持FlashPlayer就能非常简易的播放RTMP的直播,协议详解可以Google了解。相对其他协议而言,RTMP协议初次建立连接的时候握手过程过于复杂(底层基于TCP,这里说的是RTMP协议本身的交互),视不同的网络状况会带来给首开带来100ms以上的延迟。基于RTMP的直播一般内容延迟在2~5秒。
HTTP-FLV协议:
即使用HTTP协议流式的传输媒体内容。相对于RTMP,HTTP更简单和广为人知,而且不担心被Adobe的专利绑架。内容延迟同样可以做到2~5秒,打开速度更快,因为HTTP本身没有复杂的状态交互。所以从延迟角度来看,HTTP-FLV要优于RTMP。
HLS&协议:
即Http Live Streaming,是由苹果提出基于HTTP的流媒体传输协议。HLS有一个非常大的优点:HTML5可以直接打开播放;这个意味着可以把一个直播链接通过微信等转发分享,不需要安装任何独立的APP,有浏览器即可,所以流行度很高。社交直播APP,HLS可以说是刚需,下来我们分析下其原理 。
基于HLS的直播流URL是一个m3u8的文件,里面包含了最近若干个小视频TS(一种视频封装格式,这里就不扩展介绍)文件,如&http://www.ucloud.cn/helloworld.m3u8&&是一个直播留链接,其内容如下:
假设列表里面的包含5个TS文件,每个TS文件包含5秒的视频内容,那么整体的延迟就是25秒。当然可以缩短列表的长度和单个TS文件的大小来降低延迟,极致来说可以缩减列表长度为1,1秒内容的m3u8文件,但是极易受网络波动影响造成卡顿。
通过公网的验证,目前按同城网络可以做到比较好的效果是5~7秒的延迟,也是综合流畅度和内容延迟的结果。那么HTML5是否可以有更低延迟直接打开的直播流技术呢? 我们在最后会探讨这个问题。
即Real-time Transport Protocol,用于Internet上针对多媒体数据流的一种传输层协议。
实际应用场景下经常需要RTCP(RTP Control Protocol)配合来使用,可以简单理解为RTCP传输交互控制的信令,RTP传输实际的媒体数据。
RTP在视频监控、视频会议、IP电话上有广泛的应用,因为视频会议、IP电话的一个重要的使用体验:内容实时性强。
对比与上述3种或实际是2种协议,RTP和它们有一个重要的区别就是默认是使用UDP协议来传输数据,而RTMP和HTTP是基于TCP协议传输。为什么UDP 能做到如此实时的效果呢?关于TCP和UDP差别的分析文章一搜一大把,这里不在赘述,简单概括:
UDP:单个数据报,不用建立连接,简单,不可靠,会丢包,会乱序;
TCP:流式,需要建立连接,复杂,可靠&,有序。
实时音视频流的场景不需要可靠保障,因此也不需要有重传的机制,实时的看到图像声音,网络抖动时丢了一些内容,画面模糊和花屏,完全不重要。TCP为了重传会造成延迟与不同步,如某一截内容因为重传,导致1秒以后才到,那么整个对话就延迟了1秒,随着网络抖动,延迟还会增加成2秒、3秒,如果客户端播放是不加以处理将严重影响直播的体验。
总结一下:在直播协议的选择中,如果选择是RTMP或HTTP-FLV则意味着有2~5秒的内容延迟,但是就打开延迟开,HTTP-FLV 要优于RTMP。HLS则有5~7秒的内容延迟。选择RTP进行直播则可以做到1秒内的直播延迟。但就目前所了解,各大CDN厂商没有支持基于RTP直播的,所以目前国内主流还是RTMP或HTTP-FLV。
是否有除了HLS外更低延迟的方案?
HLS的优点点是显而易见的:移动端无需安装APP使用兼容HTML5的浏览器打开即可观看,所有主流的移动端浏览器基本都支持HTML5,在直播的传播和体验上有巨大的优势。
而看起来唯一的缺点:内容延迟高(这里也有很多HLS限制没有提到,比如必须是H264 AAC编码,也可认为是“缺点”之一)。如果能得到解决,那将会是直播技术非常大的一个进步。或者换个说法,有没有更低延迟可直接用链接传播的直播方案?不局限于HLS本身。
对于浏览器直接的视频互动,Google一直在推WebRTC,目前已有不少成型的产品出现,可以浏览器打开即实时对话、直播。但来看看如下的浏览器覆盖图:
非常遗憾的说,在直至iOS 9.3上的Safari仍然不能支持WebRTC。继续我们的探索,那Websocket支持度如何呢?
除了老而不化的Opera Mini外,所有的浏览器都支持WebSocket。这似乎是个好消息。梳理一下HTML5 WebSocket直播需要解决的问题:
1、后端兼容
3、解码播放
对于#1似乎不是特别大问题,对于做过RTMP转HLS、RTP来说是基本功。#2对于浏览器来说使用HTTP来传输是比较好的选项。对于#3 这里推荐一个开源的JS解码项目jsmpeg:&https://github.com/phoboslab/jsmpeg,里面已有一个用于直播的stream-server.js的NodeJS服务器。
从测试结果看,该项目的代码相对较薄,还没达到工业级的成熟度,需要大规模应用估计需要自填不少坑,有兴趣的同学可以学习研究。
以上就是直播云:直播应用层协议及传输层协议的选择以及对直播体验影响的分析 。关于接入网络优化、内容缓存与传输策略优化、终端优化,请参阅接下来发布的其他部分。
三、在传输直播流媒体过程中的内容缓存与传输策略优化细节原理
基础知识:I帧、B帧、P帧
I帧表示关键帧。你可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成。(因为包含完整画面)
P帧表示这一帧跟之前的一个关键帧(或P帧)的差别。解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据)
B帧是双向差别帧。B帧记录的是本帧与前后帧的差别(具体比较复杂,有4种情况)。换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。
B帧压缩率高,但是编解码时会比较耗费CPU,而且在直播中可能会增加直播延时,因此在移动端上一般不使用B帧。
关键帧缓存策略
一个典型的视频帧序列为IBBPBBPBBP……
对于直播而言,为了减少直播的延时,通常在编码时不使用B帧。P帧B帧对于I帧都有直接或者间接的依赖关系,所以播放器要解码一个视频帧序列,并进行播放,必须首先解码出I帧,其后续的B帧和P帧才能进行解码,这样服务端如何进行关键帧的缓存,则对直播的延时以及其他方面有非常大的影响。
比较好的策略是服务端自动判断关键帧的间隔,按业务需求缓存帧序列,保证在缓存中存储至少两个或者以上的关键帧,以应对低延时、防卡顿、智能丢包等需求。
延迟与卡顿的折中
直播的延时与卡顿是分析直播业务质量时,非常关注的两项指标。互动直播的场景对延时非常敏感,新闻体育类直播则更加关注播放的流畅度。
然而,这两项指标从理论上来说,是一对矛盾的关系——需要更低的延时,则表明服务器端和播放端的缓冲区都必须更短,来自网络的异常抖动容易引起卡顿;业务可以接受较高的延时时,服务端和播放端都可以有较长的缓冲区,以应对来自网络的抖动,提供更流畅的直播体验。
当然,对于网络条件非常好的用户,这两项是可以同时保证的,这里主要是针对网络条件不是那么好的用户,如何解决延时与卡顿的问题。
这里通常有两种技术来平衡和优化这两个指标。
一是服务端提供灵活的配置策略,对于延时要求更敏感的,则在服务端在保证关键帧的情况下,对每个连接维持一个较小的缓冲队列;对于卡顿要求更高的直播,则适当增加缓冲队列的长度,保证播放的流畅。
二是服务端对所有连接的网络情况进行智能检测,当网络状况良好时,服务端会缩小该连接的缓冲队列的大小,降低延迟;而当网络状况较差时,特别是检测到抖动较为明显时,服务端对该连接增加缓冲队列长度,优先保证播放的流畅性。
什么时候需要丢包呢?
对于一个网络连接很好,延时也比较小的连接,丢包策略永远没有用武之地的。而网络连接比较差的用户,因为下载速度比较慢或者抖动比较大,这个用户的延时就会越来越高。
另外一种情况是,如果直播流关键帧间隔比较长,那么在保证首包是关键帧的情况下,观看这个节目的观众,延迟有可能会达到一个关键帧序列的长度。上述两种情况,都需要启用丢包策略,来调整播放的延时。
关于丢包,需要解决两个问题:
一是正确判断何时需要进行丢包;
二是如何丢包以使得对观众的播放体验影响最小。较好的做法是后端周期监控所有连接的缓冲队列的长度,这样队列长度与时间形成一个离散的函数关系,后端通过自研算法来分析这个离散函数,判断是否需要丢包。
一般的丢帧策略,就是直接丢弃一个完整的视频帧序列,这种策略看似简单,但对用户播放的影响体验非常大。而应该是后台采用逐步丢帧的策略,每个视频帧序列,丢最后的一到两帧,使得用户的感知最小,平滑的逐步缩小延时的效果。
四、客户端的优化
参见之前介绍的DNS过程,如下图:
基于可控和容灾的需要,移动端代码一般不会hardcode 推流、播放的服务器IP地址,而选用域名代替。在IP出现宕机或网络中断的情况下,还可以通过变更DNS来实现问题IP的剔除。而域名的解析时间需要几十毫秒至几秒不等,对于新生成热度不高的域名,一般的平均解析延迟在300ms,按上图的各个环节只要有一个通路网络产生波动或者是设备高负载,会增加至秒级。几十毫秒的情况是ISP NS这一层在热度足够高的情况下会对域名的解析进行缓存。如下图:
按我们上面分析的情况,本省延迟大概是15ms左右,那么域名解析最低也可以做到15ms左右。但由于直播场景的特殊性,推流和播放使用的域名使用的热度较难达到ISP NS缓存的标准,所以经常需要走回Root NS进行查询的路径。
那客户端解析优化的原理就出来了:本机缓存域名的解析结果,对域名进行预解析,每次需要直播推流和播放的时候不再需要再进行DNS过程。此处节省几十到几百毫秒的打开延迟。
直播播放器的相关技术点有:直播延时、首屏时间(指从开始播放到第一次看到画面的时间)、音视频同步、软解码、硬解码。参考如下播放流程:
播放步骤描述:
根据协议类型(如RTMP、RTP、RTSP、HTTP等),与服务器建立连接并接收数据;
解析二进制数据,从中找到相关流信息;
根据不同的封装格式(如FLV、TS)解复用(demux);
分别得到已编码的H.264视频数据和AAC音频数据;
使用硬解码(对应系统的API)或软解码(FFMpeg)来解压音视频数据;
经过解码后得到原始的视频数据(YUV)和音频数据(AAC);
因为音频和视频解码是分开的,所以我们得把它们同步起来,否则会出现音视频不同步的现象,比如别人说话会跟口型对不上;
最后把同步的音频数据送到耳机或外放,视频数据送到屏幕上显示。
了解了播放器的播放流程后,我们可以优化以下几点:
首屏时间优化
从步骤2入手,通过预设解码器类型,省去探测文件类型时间;
从步骤5入手,缩小视频数据探测范围,同时也意味着减少了需要下载的数据量,特别是在网络不好的时候,减少下载的数据量能为启动播放节省大量的时间,当检测到I帧数据后就立马返回并进入解码环节。
视频缓冲区或叫视频缓存策略,该策略原理是当网络卡顿时增加用户等待时间来缓存一定量的视频数据,达到后续平滑观看的效果,该技术能有效减少卡顿次数,但是会带来直播上的内容延时,所以该技术主要运用于点播,直播方面已去掉该策略,以此尽可能去掉或缩小内容从网络到屏幕展示过程中的时间;(有利于减少延时)。
下载数据探测池技术,当用户下载速度不足发生了卡顿,然后网络突然又顺畅了,服务器上之前滞留的数据会加速发下来,这时为了减少之前卡顿造成的延时,播放器会加速播放探测池的视频数据并丢弃当前加速部分的音频数据,以此来保证当前观看内容延时稳定。
推流步骤说明:很容易看出推流跟播放其实是逆向的,具体流程就不多说了。
优化一:适当的Qos(Quality of Service,服务质量)策略。
推流端会根据当前上行网络情况控制音视频数据发包和编码,在网络较差的情况下,音视频数据发送不出去,造成数据滞留在本地,这时,会停掉编码器防止发送数据进一步滞留,同时会根据网络情况选择合适的策略控制音视频发送。
比如网络很差的情况下,推流端会优先发送音频数据,保证用户能听到声音,并在一定间隔内发关键帧数据,保证用户在一定时间间隔之后能看到一些画面的变化。
优化二:合理的关键帧配置。
合理控制关键帧发送间隔(建议2秒或1秒一个),这样可以减少后端处理过程,为后端的缓冲区设置更小创造条件。
软硬编解选择
网上有不少关于选择软解还是硬解的分析文章,这里也介绍一些经验,但根本问题是,没有一个通用方案能最优适配所有操作系统和机型。
推流编码:&推荐Andorid4.3(API18)或以上使用硬编,以下版本使用软编;iOS使用全硬编方案;
播放解码:Andorid、iOS播放器都使用软解码方案,经过我们和大量客户的测试以及总结,虽然牺牲了功耗,但是在部分细节方面表现会较优,且可控性强,兼容性也强,出错情况少,推荐使用。
附软硬编解码优缺点对比:
云端机型及网络适配
上面分析了很多针对视频编解码的参数,但实际情况最好的编解码效果是需要根据机型的适配的,由于iOS的设备类型较少,可以做到每个机型针对性的测试和调优,但是对于Android就非常难做到逐款机型针对性调优,并且每年都会出产不少的新机器,如果代码中写死了配置或判断逻辑将非常不利于维护和迭代。
所以我们就诞生了一个想法,这些判断逻辑或配置是否可以放在云上呢? &这样就产生了云端机型与网络适配的技术。
终端在推流、播放前会获取通过协议上报当前的机型配置、网络情况、IP信息。云端会返回一个已最适合的编解码策略配置:走软编还是硬编、各项参数的配置,就近推流服务的IP,就近播放服务的IP。 终端获取一次即可,不需要每次推流、播放前都去获取一次。
这样,在我们不断的迭代和完善机型编解码适配库的同时,所有使用该技术的直播APP都将收益。
分析很多直播后端、终端的关于低延迟、秒开的优化技术,在UCloud直播云上都已有了相关的实践,都是一些较“静态”的技术。实际提供稳定、低延迟、流畅的直播服务,是日常中非常大量细致的监控、算法和动态运营的结果,并不是实现了某些的技术点,就能坐享一套稳定的直播服务,只能说是完成了万里长城的第一道砖。
from:&http://blog.csdn.net/a/article/details/
在上使用FFmpeg将摄像头采集的YUV裸流编码为h264。&
二、环境准备&
1、使用FFmpeg动态库(这个动态库需要有libx264的实现,否则可能会出现寻找编码器失败异常)。关于如何获得这样的一个动态库可以参考&
2、Android开发环境(我用的是Android Studio2.2.3) 和最新的ndk。&
1、初始化ffmpeg的一些配置。&
2、调用系统摄像头设置参数使用mCamera.setPreviewCallbackWithBuffer();设置回调接口用来接受YUV数据。&
3、将摄像头获得的YUV数据(默认是NV21)转化成YUV420P格式&
3、将获得的修改后的数据传给编码器,进行编码&
流程基本分三大步&
1、初始化(包括打开输出文件,设置参数,寻找编码器、写入头信息等。)&
2、实时传入数据进行编码&
3、刷帧,并写入尾部信息。释放资源
我用三个jni方法分别对应这三步:
* 初始化。
* destUrl 目标url
public static native int init(String destUrl, int w, int h);
* 传入数据。
public static native int push(byte[] bytes,int w,int h);
public static native int stop();12345678910111213141516171819202122232425261234567891011121314151617181920212223242526
package com.blueberry.x264;
import android.app.A
import android.graphics.ImageF
import android.hardware.C
import android.os.B
import android.support.annotation.N
import android.support.v7.app.AppCompatA
import android.util.L
import android.view.S
import android.view.SurfaceH
import android.view.SurfaceV
import android.view.V
import android.widget.B
import java.io.IOE
import java.util.A
import java.util.L
import static android.hardware.Camera.Parameters.FLASH_MODE_AUTO;
import static android.hardware.Camera.Parameters.PREVIEW_FPS_MAX_INDEX;
import static android.hardware.Camera.Parameters.PREVIEW_FPS_MIN_INDEX;
* Created by blueberry on 1/3/2017.
public class CameraActivity extends AppCompatActivity implements SurfaceHolder.Callback2,
Camera.PreviewCallback {
private static final String TAG = &CameraActivity&;
private Button btnS
private SurfaceView mSurfaceV
private SurfaceHolder mSurfaceH
private Camera mC
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
btnStart = (Button) findViewById(R.id.btn_start);
mSurfaceView = (SurfaceView) findViewById(R.id.surface_view);
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(this);
btnStart.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
private boolean isP
private boolean isS
private void start() {
if (isStarted) {
isStarted = false;
isStarted = true;
isPublish = true;
Pusher.init(&/sdcard/camera.h264&, size.width, size.height);
private void stop() {
isPublish = false;
Pusher.stop();
private void openCamera() {
this.mCamera = Camera.open();
} catch (RuntimeException e) {
throw new RuntimeException(&打开摄像头失败&, e);
private Camera.S
private boolean isP
private void initCamera() {
if (this.mCamera == null) {
openCamera();
setParameters();
setCameraDisplayOrientation(this, Camera.CameraInfo.CAMERA_FACING_BACK, mCamera);
int buffSize = size.width * size.height * ImageFormat.getBitsPerPixel(ImageFormat.NV21) / 8;
mCamera.addCallbackBuffer(new byte[buffSize]);
mCamera.setPreviewCallbackWithBuffer(this);
mCamera.setPreviewDisplay(mSurfaceHolder);
} catch (IOException e) {
e.printStackTrace();
if (isPreview) {
mCamera.stopPreview();
isPreview = false;
mCamera.startPreview();
isPreview = true;
public static void setCameraDisplayOrientation(Activity activity,
int cameraId, android.hardware.Camera camera) {
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
int rotation = activity.getWindowManager().getDefaultDisplay()
.getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0:
degrees = 0;
case Surface.ROTATION_90:
degrees = 90;
case Surface.ROTATION_180:
degrees = 180;
case Surface.ROTATION_270:
degrees = 270;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360;
result = (info.orientation - degrees + 360) % 360;
camera.setDisplayOrientation(result);
private void setParameters() {
Camera.Parameters parameters = mCamera.getParameters();
List&Camera.Size& supportedPreviewSizes = parameters.getSupportedPreviewSizes();
for (Camera.Size supportSize : supportedPreviewSizes) {
if (supportSize.width &= 160 && supportSize.width &= 240) {
this.size = supportS
Log.i(TAG, &setParameters: width:& + size.width + & ,height:& + size.height);
int defFPS = 20 * 1000;
List&int[]& supportedPreviewFpsRange = parameters.getSupportedPreviewFpsRange();
int[] destRange = null;
for (int i = 0; i & supportedPreviewFpsRange.size(); i++) {
int[] range = supportedPreviewFpsRange.get(i);
if (range[PREVIEW_FPS_MAX_INDEX] &= defFPS) {
destRange =
Log.i(TAG, &setParameters: destRange:& + Arrays.toString(range));
parameters.setPreviewFpsRange(destRange[PREVIEW_FPS_MIN_INDEX],
destRange[PREVIEW_FPS_MAX_INDEX]);
parameters.setPreviewSize(size.width, size.height);
parameters.setFlashMode(FLASH_MODE_AUTO);
parameters.setPreviewFormat(ImageFormat.NV21);
mCamera.setParameters(parameters);
public void surfaceRedrawNeeded(SurfaceHolder holder) {
public void surfaceCreated(SurfaceHolder holder) {
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
initCamera();
public void surfaceDestroyed(SurfaceHolder holder) {
public void onPreviewFrame(final byte[] data, Camera camera) {
if (isPublish) {
Pusher.push(data,size.width,size.height);
int buffSize = size.width * size.height * ImageFormat.getBitsPerPixel(ImageFormat.NV21) / 8;
if (data == null) {
mCamera.addCallbackBuffer(new byte[buffSize]);
mCamera.addCallbackBuffer(data);
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
public final class Pusher {
* 初始化。
* destUrl 目标url
public static native int init(String destUrl, int w, int h);
* 传入数据。
public static native int push(byte[] bytes,int w,int h);
public static native int stop();
123456789101112131415161718192021222324252627282930123456789101112131415161718192021222324252627282930
#include &jni.h&
#include &stdio.h&
#include &android/log.h&
#include &libavutil/imgutils.h&
#include &libavformat/avformat.h&
#include &libavutil/time.h&
#define LOGI(format, ...) \
__android_log_print(ANDROID_LOG_INFO, TAG,
format, ##__VA_ARGS__)
#define LOGD(format, ...) \
__android_log_print(ANDROID_LOG_DEBUG,TAG,format,##__VA_ARGS__)
#define LOGE(format, ...) \
__android_log_print(ANDROID_LOG_ERROR,TAG,format,##__VA_ARGS__)
#define TAG &Push&
#define FPS 10
AVPacket avP
AVFrame *avF
AVStream *video_
AVCodecContext *avCodecC
int fameCount = 0;
AVFormatContext *ofmt_
int64_t start_
static int stop();
static int init(const char *destUrl, int w, int h);
static int push(uint8_t *bytes);
void callback(void *ptr, int level, const char *fmt, va_list vl);
JNIEXPORT jint JNICALL
Java_com_blueberry_x264_Pusher_init(JNIEnv *env, jclass type, jstring destUrl_, jint w, jint h) {
const char *destUrl = (*env)-&GetStringUTFChars(env, destUrl_, 0);
int ret = init(destUrl, w, h);
(*env)-&ReleaseStringUTFChars(env, destUrl_, destUrl);
JNIEXPORT jint JNICALL
Java_com_blueberry_x264_Pusher_push(JNIEnv *env, jclass type, jbyteArray bytes_, jint w, jint h) {
jbyte *bytes = (*env)-&GetByteArrayElements(env, bytes_, NULL);
int ret = push((uint8_t *) bytes);
(*env)-&ReleaseByteArrayElements(env, bytes_, bytes, 0);
JNIEXPORT jint JNICALL
Java_com_blueberry_x264_Pusher_stop(JNIEnv *env, jclass type) {
return stop();
void callback(void *ptr, int level, const char *fmt, va_list vl) {
FILE *f = fopen(&/storage/emulated/0/avlog.txt&, &a+&);
vfprintf(f, fmt, vl);
fflush(f);
fclose(f);
static int flush_encoder(AVFormatContext *fmt_ctx, int streamIndex) {
AVPacket enc_
if (!(fmt_ctx-&streams[streamIndex]-&codec-&codec-&capabilities & CODEC_CAP_DELAY)) {
while (1) {
enc_pkt.data = NULL;
enc_pkt.size = 0;
av_init_packet(&enc_pkt);
ret = avcodec_encode_video2(fmt_ctx-&streams[streamIndex]-&codec, &enc_pkt, NULL,
&got_frame);
av_frame_free(NULL);
if (ret & 0) {
if (!got_frame) {
LOGI(&Flush Encoder : Succeed to encoder 1 frame! \tsize:%5d\n&, enc_pkt.size);
ret = av_write_frame(fmt_ctx, &enc_pkt);
if (ret & 0) {
static int stop() {
ret = flush_encoder(ofmt_ctx, 0);
if (ret & 0) {
LOGE(&Flush Encoder failed&);
av_write_trailer(ofmt_ctx);
if (video_st) {
avcodec_close(video_st-&codec);
av_free(avFrame);
avio_close(ofmt_ctx-&pb);
avformat_free_context(ofmt_ctx);
LOGI(&stop----------------------&);
static int push(uint8_t *bytes) {
start_time = av_gettime();
int got_picture = 0;
static int i = 0;
int j = 0;
avFrame = av_frame_alloc();
int picture_size = av_image_get_buffer_size(avCodecContext-&pix_fmt, avCodecContext-&width,
avCodecContext-&height, 1);
uint8_t buffers[picture_size];
av_image_fill_arrays(avFrame-&data, avFrame-&linesize, buffers, avCodecContext-&pix_fmt,
avCodecContext-&width, avCodecContext-&height, 1);
av_new_packet(&avPacket, picture_size);
size = avCodecContext-&width * avCodecContext-&
memcpy(avFrame-&data[0], bytes, size);
for (j = 0; j & size / 4; j++) {
*(avFrame-&data[2] + j) = *(bytes + size + j * 2);
*(avFrame-&data[1] + j) = *(bytes + size + j * 2 + 1);
int ret = avcodec_encode_video2(avCodecContext, &avPacket, avFrame, &got_picture);
LOGD(&avcodec_encode_video2 spend time %ld&, (int) ((av_gettime() - start_time) / 1000));
if (ret & 0) {
LOGE(&Fail to avcodec_encode ! code:%d&, ret);
return -1;
if (got_picture == 1) {
avPacket.pts = i++ * (video_st-&time_base.den) / ((video_st-&time_base.num) * FPS);
LOGI(&Succeed to encode frame: %5d\tsize:%5d\n&, fameCount, avPacket.size);
avPacket.stream_index = video_st-&
avPacket.dts = avPacket.pts;
avPacket.duration = 1;
int64_t pts_time = AV_TIME_BASE * av_q2d(video_st-&time_base);
int64_t now_time = av_gettime() - start_
if (pts_time & now_time) {
LOGD(&等待&);
av_usleep(pts_time - now_time);
av_write_frame(ofmt_ctx, &avPacket);
LOGD(&av_write_frame spend time %ld&, (int) (av_gettime() - start_time) / 1000);
av_free_packet(&avPacket);
fameCount++;
LOGE(&唉~&);
av_frame_free(&avFrame);
static int init(const char *destUrl, int w, int h) {
av_log_set_callback(callback);
av_register_all();
LOGD(&resister_all&);
AVOutputFormat *
LOGI(&ouput url: %s&, destUrl);
avformat_alloc_output_context2(&ofmt_ctx, NULL, &flv&, destUrl);
LOGD(&allocl ofmt_ctx finished&);
fmt = ofmt_ctx-&
if ((ret = avio_open(&ofmt_ctx-&pb, destUrl, AVIO_FLAG_READ_WRITE)) & 0) {
LOGE(&avio_open error&);
return -1;
video_st = avformat_new_stream(ofmt_ctx, NULL);
if (video_st == NULL) {
return -1;
LOGD(&new stream finished&);
avCodecContext = video_st-&
avCodecContext-&codec_id = AV_CODEC_ID_H264;
avCodecContext-&codec_type = AVMEDIA_TYPE_VIDEO;
avCodecContext-&pix_fmt = AV_PIX_FMT_YUV420P;
avCodecContext-&width =
avCodecContext-&height =
avCodecContext-&bit_rate = 400000;
avCodecContext-&gop_size = 250;
avCodecContext-&time_base.num = 1;
avCodecContext-&time_base.den = FPS;
avCodecContext-&qmin = 10;
avCodecContext-&qmax = 51;
avCodecContext-&max_b_frames = 3;
AVDictionary *param = 0;
if (avCodecContext-&codec_id == AV_CODEC_ID_H264) {
av_dict_set(&param, &preset&, &slow&, 0);
av_dict_set(&param, &tune&, &zerolatency&, 0);
LOGI(&set h264 param finished&);
if (avCodecContext-&codec_id == AV_CODEC_ID_H265) {
av_dict_set(&param, &preset&, &ultrafast&, 0);
av_dict_set(&param, &tune&, &zero-latency&, 0);
LOGI(&set h265 param&);
AVCodec *avC
avCodec = avcodec_find_encoder(avCodecContext-&codec_id);
if (NULL == avCodec) {
LOGE(&寻找编码器失败..&);
return -1;
if ((ret = avcodec_open2(avCodecContext, avCodec, &param)) & 0) {
LOGE(&avcodec_open2 fail!&);
return -1;
avformat_write_header(ofmt_ctx, NULL);
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
注意这里我设置的帧率为10帧,并且设置的预览宽度不超过240,主要目的就是为了提高编码的效率。(我用小米2S这样勉强可以编码),测试这个方法avcodec_encode_video2&
非常的耗时,如果我是1080P的图像时他可能需要等待1秒钟!!!目前刚接触ffmpeg不知道有什么办法可以优化。如有知道的大神可以指点指点。
from:&http://blog.csdn.net/a/article/details/
上一篇文章我讲到,我用libx264对视频进行h264编码效率非常低下,原因在于libx264采用的是软编码。于是我在网上搜索得知使用系统的API可以对视频进行硬编码,从而减少cpu的压力,达到提高效率的作用。我写了一个demo试了一下,果真效率提高的很明显。&
MediaCodec&
这个类用来进行音/视频编码。
AudioRecord&
这个类用来录音得到PCM音频数据。
MediaMuxer&
这个类用来将编码好的音视频数据写入文件。Camera&
用来采集摄像头的数据。
三、核心实现&
1、初始化视频编码器
private void initVideoEncoder() {
MediaCodecInfo mediaCodecInfo = selectCodec(VCODEC_MIME)
colorFormat = getColorFormat(mediaCodecInfo)
vencoder = MediaCodec.createByCodecName(mediaCodecInfo.getName())
Log.d(TAG, &编码器:& + mediaCodecInfo.getName() + &创建完成!&)
} catch (IOException e) {
e.printStackTrace()
throw new RuntimeException(&vencodec初始化失败!&, e)
https://developer.android.google.cn/reference/android/media/MediaFormat.html
MediaFormat mediaFormat = MediaFormat
.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, previewSize.width, previewSize.height)
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0)
mediaFormat.setInteger(KEY_BIT_RATE, 300 * 1000)
mediaFormat.setInteger(KEY_COLOR_FORMAT, colorFormat)
mediaFormat.setInteger(KEY_FRAME_RATE, 30)
mediaFormat.setInteger(KEY_I_FRAME_INTERVAL, 5)
vencoder.configure(mediaFormat, null, null, CONFIGURE_FLAG_ENCODE)
vencoder.start()
}12345678910111213141516171819202122231234567891011121314151617181920212223
2、音频编码器初始化
private void initAudioEncoder() {
aEncoder = MediaCodec.createEncoderByType(ACODEC)
} catch (IOException e) {
e.printStackTrace()
throw new RuntimeException(&初始化音频编码器失败&, e)
Log.d(TAG, String.format(&编码器:%s创建完成&, aEncoder.getName()))
MediaFormat aformat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
aSampleRate, aChannelCount)
aformat.setInteger(KEY_BIT_RATE, 1000 * ABITRATE_KBPS)
aformat.setInteger(KEY_MAX_INPUT_SIZE, 0)
aEncoder.configure(aformat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
aloop = true
mAudioRecord.startRecording()
audioWorkThread=new Thread(fetchAudioRunnable)
audioWorkThread.start()
aEncoder.start()
}123456789101112131415161718192021123456789101112131415161718192021
3、视频编码
private void onGetVideoFrame(byte[] i420) {
ByteBuffer[] inputBuffers = vencoder.getInputBuffers();
ByteBuffer[] outputBuffers = vencoder.getOutputBuffers();
int inputBufferId = vencoder.dequeueInputBuffer(-1);
if (inputBufferId &= 0) {
ByteBuffer bb = inputBuffers[inputBufferId];
bb.clear();
bb.put(i420, 0, i420.length);
long pts = new Date().getTime() * 1000 - presentationTimeUs;
vencoder.queueInputBuffer(inputBufferId, 0, i420.length, pts, 0);
for (; ; ) {
int outputBufferId = vencoder.dequeueOutputBuffer(vBufferInfo, 0);
if (outputBufferId &= 0) {
ByteBuffer bb = outputBuffers[outputBufferId];
onEncodedh264Frame(bb, vBufferInfo);
vencoder.releaseOutputBuffer(outputBufferId, false);
if (outputBufferId & 0) {
}1234567891011121314151617181920212223242526272812345678910111213141516171819202122232425262728
4、音频编码
private void onGetPcmFrame(byte[] data) {
ByteBuffer[] inputBuffers = aEncoder.getInputBuffers();
ByteBuffer[] outputBuffers = aEncoder.getOutputBuffers();
int inputBufferId = aEncoder.dequeueInputBuffer(-1);
if (inputBufferId &= 0) {
ByteBuffer bb = inputBuffers[inputBufferId];
bb.clear();
bb.put(data, 0, data.length);
long pts = new Date().getTime() * 1000 - presentationTimeUs;
aEncoder.queueInputBuffer(inputBufferId, 0, data.length, pts, 0);
for (; ; ) {
int outputBufferId = aEncoder.dequeueOutputBuffer(aBufferInfo, 0);
if (outputBufferId &= 0) {
ByteBuffer bb = outputBuffers[outputBufferId];
onEncodeAacFrame(bb, aBufferInfo);
aEncoder.releaseOutputBuffer(outputBufferId, false);
if (outputBufferId & 0) {
}1234567891011121314151617181920212223242512345678910111213141516171819202122232425
5、写入文件
private void onEncodeAacFrame(ByteBuffer bb, MediaCodec.BufferInfo info) {
mediaMuxer.writeSampleData(audioTrackIndex, bb, info);
private void onEncodedh264Frame(ByteBuffer es, MediaCodec.BufferInfo bi) {
mediaMuxer.writeSampleData(videoTrackIndex, es, bi);
}1234567812345678
上述都是一些核心代码,因为这些代码都比较偏底层,所以看起来都比较难记,不过这都是参考的官网文档的示,我从文档上copy下来后稍作修改得到的。
另外注意一下,从摄像头采取的音频数据默认是NV21格式的,如果不做变换就进行转码的话,你得到视频可能会颜色失真。这里我是这样处理的,我先得到编码器支持的像素格式,代码如下:
private int getColorFormat(MediaCodecInfo mediaCodecInfo) {
int matchedFormat = 0;
MediaCodecInfo.CodecCapabilities codecCapabilities =
mediaCodecInfo.getCapabilitiesForType(VCODEC_MIME);
for (int i = 0; i & codecCapabilities.colorFormats.length; i++) {
int format = codecCapabilities.colorFormats[i];
if (format &= codecCapabilities.COLOR_FormatYUV420Planar &&
format &= codecCapabilities.COLOR_FormatYUV420PackedSemiPlanar) {
if (format &= matchedFormat) {
matchedFormat = format;
logColorFormatName(format);
return matchedF
}12345678910111213141516171234567891011121314151617
然后使用mediaFormat.setInteger(KEY_COLOR_FORMAT, colorFormat);设置颜色格式,&
最后在onPreviewFrame回调函数中根据编码器器支持的颜色格式进行转换,比如我的手机支持COLOR_FormatYUV420SemiPlanar这个格式那么我就将Nv21数据转换成 yuv420格式
public void onPreviewFrame(byte[] data, Camera camera) {
if (isStarted) {
if (data != null) {
// data 是Nv21
if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar) {
Yuv420Util.Nv21ToYuv420SP(data, dstByte, previewSize.width, previewSize.height);
} else if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar) {
Yuv420Util.Nv21ToI420(data, dstByte, previewSize.width, previewSize.height);
} else if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible) {
// Yuv420_888
} else if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar) {
// Yuv420packedPlannar 和 yuv420sp很像
// 区别在于 加入 width = 4的话 y1,y2,y3 ,y4公用 u1v1
// 而 yuv420dp 则是 y1y2y5y6 共用 u1v1
//http://blog.csdn.net/jumper511/article/details/
//这样处理的话颜色核能会有些失真。
Yuv420Util.Nv21ToYuv420SP(data, dstByte, previewSize.width, previewSize.height);
System.arraycopy(data, 0, dstByte, 0, data.length);
onGetVideoFrame(dstByte);
camera.addCallbackBuffer(data);
camera.addCallbackBuffer(new byte[calculateLength(ImageFormat.NV21)]);
}123456789101112131415161718192021222324252627282930123456789101112131415161718192021222324252627282930
MediaCodec用法参考:
YUV420:它每个像素有Y、U、V组成 ,Y代表亮度,U,V决定色度。 YUV420表示原数据中Y:U:V 为 4:1:1 。YUV420之下还分为:YUV420P(NV21) 、YUV420SP、NV12、等等。&
大家可以自行百度。
四、完整代码&
MainActivity.
package com.blueberry.
import android.app.A
import android.graphics.ImageF
import android.hardware.C
import android.media.AudioF
import android.media.AudioR
import android.media.MediaC
import android.media.MediaCodecI
import android.media.MediaCodecL
import android.media.MediaF
import android.media.MediaM
import android.media.MediaR
import android.os.B
import android.os.B
import android.support.annotation.RequiresA
import android.support.v7.app.AppCompatA
import android.util.L
import android.view.S
import android.view.SurfaceH
import android.view.SurfaceV
import android.view.V
import android.widget.B
import android.widget.EditT
import android.widget.T
import java.io.IOE
import java.nio.ByteB
import java.util.A
import java.util.D
import java.util.L
import static android.hardware.Camera.Parameters.FOCUS_MODE_AUTO;
import static android.hardware.Camera.Parameters.PREVIEW_FPS_MAX_INDEX;
import static android.hardware.Camera.Parameters.PREVIEW_FPS_MIN_INDEX;
import static android.media.MediaCodec.CONFIGURE_FLAG_ENCODE;
import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiP
import static android.media.MediaFormat.KEY_BIT_RATE;
import static android.media.MediaFormat.KEY_COLOR_FORMAT;
import static android.media.MediaFormat.KEY_FRAME_RATE;
import static android.media.MediaFormat.KEY_I_FRAME_INTERVAL;
import static android.media.MediaFormat.KEY_MAX_INPUT_SIZE;
* https://developer.android.google.cn/reference/android/media/MediaCodec.html#dequeueInputBuffer(long)
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback2 {
private static final String TAG = &MainActivity&;
private static final String VCODEC_MIME = &video/avc&;
private static final String ACODEC = &audio/mp4a-latm&;
private EditText etO
private Button btnS
private SurfaceView mSurfaceV
private SurfaceHolder mSurfaceH
private Camera mC
private Camera.Size previewS
private boolean isS
private int videoTrackI
private int audioTrackI
private int colorF
private long presentationTimeUs;
private AudioRecord mAudioR
private MediaCodec.BufferInfo vBufferInfo = new MediaCodec.BufferInfo();
private MediaCodec.BufferInfo aBufferInfo = new MediaCodec.BufferInfo();
private MediaC
private MediaMuxer mediaM
private int aSampleR
private int
private int aChannelC
private byte[]
private static final int ABITRATE_KBPS = 30;
private MediaCodec aE
private boolean
private Thread audioWorkT
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etOutput = (EditText) findViewById(R.id.et_output_url);
btnStart = (Button) findViewById(R.id.btn_start);
mSurfaceView = (SurfaceView) findViewById(R.id.surface_view);
mSurfaceView.setKeepScreenOn(true);
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(this);
btnStart.setOnClickListener(new View.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public void onClick(View v) {
codecToggle();
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
private void codecToggle() {
if (isStarted) {
btnStart.setText(isStarted ? &停止& : &开始&);
private void start() {
isStarted = true;
if (mCamera != null) {
initVideoEncoder();
initAudioDevice();
initAudioEncoder();
presentationTimeUs = new Date().getTime() * 1000;
mediaMuxer = new MediaMuxer(etOutput.getText().toString().trim(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
videoTrackIndex = mediaMuxer.addTrack(vencoder.getOutputFormat());
audioTrackIndex = mediaMuxer.addTrack(aEncoder.getOutputFormat());
mediaMuxer.start();
} catch (IOException e) {
e.printStackTrace();
private void initAudioEncoder() {
aEncoder = MediaCodec.createEncoderByType(ACODEC);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(&初始化音频编码器失败&, e);
Log.d(TAG, String.format(&编码器:%s创建完成&, aEncoder.getName()));
MediaFormat aformat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
aSampleRate, aChannelCount);
aformat.setInteger(KEY_BIT_RATE, 1000 * ABITRATE_KBPS);
aformat.setInteger(KEY_MAX_INPUT_SIZE, 0);
aEncoder.configure(aformat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
aloop = true;
mAudioRecord.startRecording();
audioWorkThread=new Thread(fetchAudioRunnable);
audioWorkThread.start();
aEncoder.start();
private Runnable fetchAudioRunnable = new Runnable() {
public void run() {
fetchAudioFromDevice();
private void fetchAudioFromDevice() {
Log.d(TAG, &录音线程开始&);
while (aloop && mAudioRecord != null && !Thread.interrupted()) {
int size = mAudioRecord.read(abuffer, 0, abuffer.length);
if (size & 0) {
Log.i(TAG, &audio ignore,no data to read.&);
if (aloop) {
byte[] audio = new byte[size];
System.arraycopy(abuffer, 0, audio, 0, size);
onGetPcmFrame(audio);
Log.d(TAG, &录音线程结束&);
private void initAudioDevice() {
int[] sampleRates = {44100, 22050, 16000, 11025};
for (int sampleRate : sampleRates) {
int audioForamt = AudioFormat.ENCODING_PCM_16BIT;
int channelConfig = AudioFormat.CHANNEL_CONFIGURATION_STEREO;
int buffsize = 2 * AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioForamt);
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
sampleRate, channelConfig, audioForamt, buffsize);
if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, &initialized the mic failed&);
aSampleRate = sampleR
abits = audioF
aChannelCount = channelConfig == AudioFormat.CHANNEL_CONFIGURATION_STEREO ? 2 : 1;
abuffer = new byte[Math.min(4096, buffsize)];
private void stop() {
if (!isStarted) return;
audioWorkThread.interrupt();
aloop = false;
aEncoder.stop();
aEncoder.release();
vencoder.stop();
vencoder.release();
mAudioRecord.stop();
mAudioRecord.release();
mediaMuxer.stop();
mediaMuxer.release();
} catch (Exception e) {
isStarted = false;
private void initVideoEncoder() {
MediaCodecInfo mediaCodecInfo = selectCodec(VCODEC_MIME);
colorFormat = getColorFormat(mediaCodecInfo);
vencoder = MediaCodec.createByCodecName(mediaCodecInfo.getName());
Log.d(TAG, &编码器:& + mediaCodecInfo.getName() + &创建完成!&);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(&vencodec初始化失败!&, e);
MediaFormat mediaFormat = MediaFormat
.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, previewSize.width, previewSize.height);
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);
mediaFormat.setInteger(KEY_BIT_RATE, 300 * 1000);
mediaFormat.setInteger(KEY_COLOR_FORMAT, colorFormat);
mediaFormat.setInteger(KEY_FRAME_RATE, 30);
mediaFormat.setInteger(KEY_I_FRAME_INTERVAL, 5);
vencoder.configure(mediaFormat, null, null, CONFIGURE_FLAG_ENCODE);
vencoder.start();
private int getColorFormat(MediaCodecInfo mediaCodecInfo) {
int matchedFormat = 0;
MediaCodecInfo.CodecCapabilities codecCapabilities =
mediaCodecInfo.getCapabilitiesForType(VCODEC_MIME);
for (int i = 0; i & codecCapabilities.colorFormats. i++) {
int format = codecCapabilities.colorFormats[i];
if (format &= codecCapabilities.COLOR_FormatYUV420Planar &&
format &= codecCapabilities.COLOR_FormatYUV420PackedSemiPlanar) {
if (format &= matchedFormat) {
matchedFormat =
logColorFormatName(format);
return matchedF
private void logColorFormatName(int format) {
switch (format) {
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible:
Log.d(TAG, &COLOR_FormatYUV420Flexible&);
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
Log.d(TAG, &COLOR_FormatYUV420PackedPlanar&);
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
Log.d(TAG, &COLOR_FormatYUV420Planar&);
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
Log.d(TAG, &COLOR_FormatYUV420PackedSemiPlanar&);
case COLOR_FormatYUV420SemiPlanar:
Log.d(TAG, &COLOR_FormatYUV420SemiPlanar&);
private static MediaCodecInfo selectCodec(String mimeType) {
int numCodecs = MediaCodecList.getCodecCount();
for (int i = 0; i & numC i++) {
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
if (!codecInfo.isEncoder()) {
String[] types = codecInfo.getSupportedTypes();
for (int j = 0; j & types. j++) {
if (types[j].equalsIgnoreCase(mimeType)) {
return codecI
return null;
protected void onResume() {
super.onResume();
initCamera();
protected void onPause() {
super.onPause();
if (mCamera != null) {
mCamera.setPreviewCallbackWithBuffer(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
protected void onDestroy() {
super.onDestroy();
private void initCamera() {
openCamera();
setParameters();
setCameraDisplayOrientation(this, Camera.CameraInfo.CAMERA_FACING_BACK, mCamera);
mCamera.setPreviewDisplay(mSurfaceHolder);
} catch (IOException e) {
e.printStackTrace();
mCamera.startPreview();
mCamera.addCallbackBuffer(new byte[calculateLength(ImageFormat.NV21)]);
mCamera.setPreviewCallback(getPreviewCallBack());
private void openCamera() throws RuntimeException {
if (mCamera == null) {
mCamera = Camera.open();
} catch (Exception e) {
Log.e(TAG, &摄像头打开失败&);
e.printStackTrace();
Toast.makeText(this, &摄像头不可用!&, Toast.LENGTH_LONG).show();
Thread.sleep(2000);
} catch (InterruptedException e1) {
throw new RuntimeException(e);
private int calculateLength(int format) {
return previewSize.width * previewSize.height
* ImageFormat.getBitsPerPixel(format) / 8;
public static void setCameraDisplayOrientation(Activity activity,
int cameraId, android.hardware.Camera camera) {
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
int rotation = activity.getWindowManager().getDefaultDisplay()
.getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0:
degrees = 0;
case Surface.ROTATION_90:
degrees = 90;
case Surface.ROTATION_180:
degrees = 180;
case Surface.ROTATION_270:
degrees = 270;
if (info.facing == Camera.CameraInfo.CAMER

我要回帖

更多关于 wwW555COm 的文章

 

随机推荐