随着无线网络和智能手机的发展,智能手机与人们日常生活联系越来越紧密,娱乐、商务应用、金融应用、交通出行各种功能的软件大批涌现,使得人们的生活丰富多彩、快捷便利,也让它成为人们生活中不可取代的一部分。其中,多媒体由于其直观性和实时性,应用范围越来越广,视频的解码与播放也就成为研究的热点。
H.264标准技术日渐成熟,采用了统一的VLC符号编码,高精度、多模式的位移估计,基于4×4块的整数变换、分层的编码语法等。这些措施使得H.264算法具有很高的编码效率,在相同的重建图像质量下,能够比H.263节约50%左右的码率。而且H.264的码流结构网络适应性强,增加了差错恢复能力。正好适用于带宽受限,差错率高的无线网络。
本文结合ffmpeg开源代码中的解码方法,采用多线程接收数据包,多级缓冲数据,接收和解码并行双线程操作等方法,缓解了由于传输的数据量大、速度快而导致的数据堵塞、解码出错、视频画面迟钝、延迟等问题。使得h.264视频的传输速度快,稳定性好。最终实现了pc端到android手机端的视频传输,以及在android手机端的解码播放。
该技术可以应用于视频会议、视频监控等应用中。
一、 H.264视频传输播放系统的总体结构
H.264视频传输播放系统分为服务器端和客户端2个部分,服务器端负责读取H.264的视频数据,并且以RTP/RTCP格式打包发送给客户端,并且接受客户端的反馈,对传输速度等作相应的控制。Android手机客户端主要完成从服务器端接收实时码流数据,经过缓冲,进行视频数据解析,然后送去解码,最后在手机上显示播放。服务器端采用c语言实现,客户端主要用java语言实现。
二、关键技术及其实现
1.基于RTP协议的打包及解包
(1)单个NAL打包
H.264NALU单元常由[start code][NALU header][NALU payload]三部分组成,其中start code 用于标志一个NALU单元的开始,必须是“00000001”或者是“000001”,打包时去掉开始码,把其他数据打包到RTP包就可以了。
(2)分片打包
由于1500个字节是IP数据报的长度的上限,去除20个字节的数据报首部,1480字节是用来存放UDP数据报的。所以当一帧中的字节数超过这个数值时,我们必须将其分片打包。而且UDP在传输的过程中也要由包头开销,所以将RTP包的最大字节数定位1400字节。
需要分片的包格式有所区别,首先说明下分片的格式:
FU指示字节有以下格式:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
FU指示字节的类型28,29表示FU-A和FU-B。NRI域的值必须根据要分片的NAL单元NRI的值设置。
FU头的格式如下:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E|R| Type |
+---------------+
S:开始位(1bit),当设置为1,开始位指示分片NAL单元的开始。第一个分片包设为1,其他的分片设置为0。
E:结束位(1bit),当设置为1,结束位指示分片NAL单元的结束,即,FU荷载是最后分片时设置为1,其他时候设置为0。
R:保留位(1bit),必须设置为0。
Type:5bit
(3)打包和解包的流程分析:
打包:
分片时详细说明:
①第一个FU-A包的FU indicator 是这么设置的:F=NALU头中的F,NRI=NALU头中的NRI,Type=28 FU header: S=1,E=0,R=0,Type=NALU头中的Type;
②中间的FU-A包的FU indicator是这么设置的:F=NALU头中的F,NRI=NALU头中的NRI,Type=28 FU header: S=0,E=0,R=0,Type=NALU头中的Type;
③尾FU-A包的FU indicator是这么设置的:F=NALU头中的F,NRI=NALU头中的NRI,Type=28 FU header: S=0,E=1,R=0,Type=NALU头中的Type。
解包:
下面我们针对RTP解包时对待分片进行分类的代码实现做分析:
byte startBit=(byte)(recbuf[13]&0x80); byte endBit=(byte)(recbuf[13]&0x40);
①如果,startBit==-128,这包是分片的首包。
NalBuf[4]=(byte) ((recbuf[12]&0xE0)+(recbuf[13]&0x1F)); 这句用于重建组合NAL单元类型
②如果(startBit==0)&&(endBit==0),这包是分片的中间部分。
③如果 endBit==64 ,这包是分片尾部。
当分类清楚,就可以对各部分做相应的处理,如图中分析的那样。
2.码流管理机制
(1) 码流的接收。在发送端码流发送很快的情况下,由于接收端不仅要接收码流,还要进行分析,解码,这个处理需要一个较长的过程,如果接收端顺序执行这个过程的话,会导致无法完整接收发送端的包、出现丢包,由此而带来的是解码错误、无法正常播放视频、甚至程序奔溃等严重错误。针对这个问题我们采取并发的处理机制予以解决。线程并发存在的一个意义就是为了提高运行在单处理器上的速度。在java中我们采用java.util.concurrent包中的执行器(Executor)来管理线程Thread对象。我们创建20个线程,也就是向SingleThreadExecutor提交了20个任务,这些任务将排好队,每个任务会在下一个任务开始之前运行结束,每个任务都是按照他们被提交的顺序,在下一个任务开始之前完成。这样不仅实现了快速的接收而且还保证了接收到的包顺序是正确的。通过这样的处理后,接收和分析解码可以被分成两个部分,我们可以把接收到的数据暂时存放在缓冲区,然后就可以接着去接收下一包数据,不用等着分析、解码完成后才去接收下一包数据。这样做大大提高了接收效率,同时避免了丢包问题。
(2) 视频数据解析和解码。由于采用了并发的机制,接收到的数据不止一包,所以对接收到的数据应该做怎样合理的处理,成为我们接下来的难点。我们需要保证的仍然是数据包的顺序,还且每次只能处理一包,这里涉及到一个线程之间的协作问题。我们采用消费者生产者这种线程协作模式来做处理。我们将从存放数据的缓冲区中按顺序取到的包经过分析后放入另外一个缓冲区,通知解码程序可以进行从此缓冲区中获得数据解码,然后分析视频数据的程序进入等待。解码完成后,通知分析视频数据的程序继续进行视频数据分析,同时解码程序又进入等待。两个程序在执行和等待中交替进行。
(3) 多级缓冲机制。上面我们也提到了几个缓冲,总结如下。
①接收后存放数据的缓冲,由于服务器端源源不断的实时码流,和采用了并发机制后带来更大量的数据,我们不可能马上处理完,所以必须设置一个缓冲区。
②接收端和处理端之间的缓冲,由于网络不稳定,接收到的数据可能会有时快有时慢,这直接会造成解码的不稳定和视频播放的不连续,所以在此设置一个缓冲,起到一个平滑,过渡的作用,这个缓冲区既要存放接收到大量的码流还要为视频数据分析提供数据,有个写读入和读出的过程,所以我们使用先入先出的队列Queue容器来做缓冲区。
③解析和解码过程之间的缓冲,由于在此过程中的数据量相较而言不是很大,而这个获取数据的速度直接影响了解码的速度,所以我们要用一个高效的缓冲区来担当此时的缓冲作用,由于stack是由系统自动分配,所以速度比较快,所以我们就在栈上分配一个数组用于存储即可。
④解码后到播放之间的缓冲,这个缓冲区同样除了起到使播放视频连续稳定的作用外,主要就是用来显示图像,还可以对视频图像进行一些处理工作,平滑,滤波等。
3.解码和播放的实现
H.264解码是移植了ffmpeg 中的H.264解码部分到Android,并且了深度删减优化。界面部分,文件接收处理以及视频显示都是用java做的,底层的视频解码部分则使用C来做从而满足速度的要求。H.264码流分割NAl(接受到视频数据的复原工作)是在java层做而没有分装到c中,是因为每次送的数据会受到限制,如果送的数据量大,底层可能会一次解码好几帧视频,但是到界面层只能显示一帧,造成丢帧。如果每次送的数据量较少,就会使得多次底层调用但并没有进行实质解码的现象发生,所以尽管这样做耦合度差些,速度慢些,但是综合考虑还是将数据分析工作放在java层完成。
我们将解码后的视频数据用bitmap显示,draw到surfaceView的方法显示到手机屏上,由于有些手机不支持rgb24但几乎所有手机都支持rgb565,所以解码后返回的是rgb565数据。
4.程序流程功能架构
三、结束语
本文完整的设计并实现了从pc端到android手机端的H.264视频传输与解码播放功能。详细的分析了实现中的技术要点和难点,详细分析了rtp打包,解包的流程,针对发送数据快而处理速度慢的问题,采用多线程并发机制予以解决,面对大量,而且不稳定的数据包,针对各个环节的特点,设置了多级缓冲。使得视频播放更加流畅、平稳。对于分析和解码的先后次序问题,采用线程协作的思想,利用消费者,生产者模式,保证了视频数据的时序性。另外对于解码部分,则利用现有解码方法进行平台移植,合理处理c层和java层的分工,并以实现了这个完整的功能。在网络状况好的情况下,android手机端视频播放延时短,播放流畅,平稳。本文技术研究可以运用到视频播放的各个应用中,有着很强的实用价值。
文/南京邮电大学 张永芹 龚建荣