打包
视音频在传输过程中需要定义相应的格式,这样传输到对端的时候才能正确地被解析出来。
1、HTTP-FLV
Web 2.0时代,要说什么类型网站最火,自然是以国外的Youtube,国内的优酷、土豆网站了。这类网站提供的视频内容可谓各有千秋,但它们无一例外的都使用了Flash作为视频播放载体,支撑这些视频网站的技术基础就是——Flash 视频(FLV) 。FLV 是一种全新的流媒体视频格式,它利用了网页上广泛使用的Flash Player 平台,将视频整合到Flash动画中。也就是说,网站的访问者只要能看Flash动画,自然也能看FLV格式视频,而无需再额外安装其它视频插件,FLV视频的使用给视频传播带来了极大便利。
HTTP-FLV即将音视频数据封装成FLV,然后通过HTTP协议传输给客户端。而作为上传端只需要将FLV格式的视音频传输到服务器端即可。
一般来说FLV格式的视音频,里面视频一般使用h264格式,而音频一般使用AAC-LC格式。
FLV格式是先传输FLV头信息,然后传输带有视音频参数的元数据(Metadata),然后传输视音频的参数信息,然后传输视音频数据。
2、RTMP
RTMP是Real Time Messaging Protocol(实时消息传输协议)的首字母缩写。该协议基于TCP,是一个协议簇,包括RTMP基本协议及RTMPT/RTMPS/RTMPE等多种变种。RTMP是一种设计用来进行实时数据通信的网络协议,主要用来在Flash/AIR平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。
RTMP协议是Adobe公司推出的实时传输协议,主要用于基于flv格式的音视频流的实时传输。得到编码后的视音频数据后,先要进行FLV包装,然后封包成rtmp格式,然后进行传输。
使用RTMP格式进行传输,需要先连接服务器,然后创建流,然后发布流,然后传输相应的视音频数据。整个发送是用消息来定义的,rtmp定义了各种形式的消息,而为了消息能够很好地发送,又对消息进行了分块处理,整个协议较为复杂。
差网络处理
好的网络下视音频能够得到及时的发送,不会造成视音频数据在本地的堆积,直播效果流畅,延时较小。而在坏的网络环境下,视音频数据发送不出去,则需要我们对视音频数据进行处理。差网络环境下对视音频数据一般有四种处理方式:缓存区设计、网络检测、丢帧处理、降码率处理。
1、缓冲区设计
视音频数据传入缓冲区,发送者从缓冲区获取数据进行发送,这样就形成了一个异步的生产者消费者模式。生产者只需要将采集、编码后的视音频数据推送到缓冲区,而消费者则负责从这个缓冲区里面取出数据发送。
2、网络检测
差网络处理过程中一个重要的过程是网络检测,当网络变差的时候能够快速地检测出来,然后进行相应的处理,这样对网络反应就比较灵敏,效果就会好很多。
我们这边通过实时计算每秒输入缓冲区的数据和发送出去数据,如果发送出去的数据小于输入缓冲区的数据,那么说明网络带宽不行,这时候缓冲区的数据会持续增多,这时候就要启动相应的机制。
3、丢帧处理
当检测到网络变差的时候,丢帧是一个很好的应对机制。视频经过编码后有关键帧和非关键帧,关键帧也就是一副完整的图片,而非关键帧描述图像的相对变化。
丢帧策略多钟多样,可以自行定义,一个需要注意的地方是:如果要丢弃P帧(非关键帧),那么需要丢弃两个关键帧之间的所有非关键帧,不然的话会出现马赛克。对于丢帧策略的设计因需求而异,可以自行进行设计。
4、降码率
在Android中,如果使用了硬编进行编码,在差网络环境下,我们可以实时改变硬编的码率,从而使直播更为流畅。当检测到网络环境较差的时候,在丢帧的同时,我们也可以降低视音频的码率。在Android sdk版本大于等于19的时候,可以通过传递参数给MediaCodec,从而改变硬编编码器出来数据的码率。
(2)封装
沿用前面的比喻,封装可以理解为采用哪种货车去运输,也就是媒体的容器。 所谓容器,就是把编码器生成的多媒体内容(视频,音频,字幕,章节信息等)混合封装在一起的标准。容器使得不同多媒体内容同步播放变得很简单,而容器的另一个作用就是为多媒体内容提供索引,也就是说如果没有容器存在的话一部影片你只能从一开始看到最后,不能拖动进度条,而且如果你不自己去手动另外载入音频就没有声音。
下面是几种常见的封装格式:
1)AVI 格式(后缀为 .avi)
2)DV-AVI 格式(后缀为 .avi)
3)QuickTime File Format 格式(后缀为 .mov)
4)MPEG 格式(文件后缀可以是 .mpg .mpeg .mpe .dat .vob .asf .3gp .mp4等)
5)WMV 格式(后缀为.wmv .asf)
6)Real Video 格式(后缀为 .rm .rmvb)
7)Flash Video 格式(后缀为 .flv)
8)Matroska 格式(后缀为 .mkv)
9)MPEG2-TS 格式 (后缀为 .ts) 目前,我们在流媒体传输,尤其是直播中主要采用的就是 FLV 和 MPEG2-TS 格式,分别用于 RTMP/HTTP-FLV 和 HLS 协议。
4.推流到服务器
推流是直播的第一公里,直播的推流对这个直播链路影响非常大,如果推流的网络不稳定,无论我们如何做优化,观众的体验都会很糟糕。所以也是我们排查问题的第一步,如何系统地解决这类问题需要我们对相关理论有基础的认识。 推送协议主要有三种:
RTSP(Real Time Streaming Protocol):实时流传送协议,是用来控制声音或影像的多媒体串流协议, 由Real Networks和Netscape共同提出的; RTMP(Real Time Messaging Protocol):实时消息传送协议,是Adobe公司为Flash播放器和服务器之间音频、视频和数据传输 开发的开放协议; HLS(HTTP Live Streaming):是苹果公司(Apple Inc.)实现的基于HTTP的流媒体传输协议; RTMP协议基于 TCP,是一种设计用来进行实时数据通信的网络协议,主要用来在 flash/AIR 平台和支持 RTMP 协议的流媒体/交互服务器之间进行音视频和数据通信。支持该协议的软件包括 Adobe Media Server/Ultrant Media Server/red5 等。 它有三种变种:
RTMP工作在TCP之上的明文协议,使用端口1935; RTMPT封装在HTTP请求之中,可穿越防火墙; RTMPS类似RTMPT,但使用的是HTTPS连接; RTMP 是目前主流的流媒体传输协议,广泛用于直播领域,可以说市面上绝大多数的直播产品都采用了这个协议。 RTMP协议就像一个用来装数据包的容器,这些数据可以是AMF格式的数据,也可以是FLV中的视/音频数据。一个单一的连接可以通过不同的通道传输多路网络流。这些通道中的包都是按照固定大小的包传输的。
服务器流分发
直播CDN分发网络
CDN,中文名称是内容分发网络,可以用来分发直播、点播、网页静态文件、小文件等等,几乎我们日常用到的互联网产品都是有CDN在背后提供支持。现在有很多公司在提供云服务,这是在CDN的基础上,提供了更丰富的一站式接入的云服务能力。例如PP云服务为客户提供直播、点播、静态文件、短视频等多种云服务和CDN加速能力。
概念:负载均衡、CDN缓存、回源、就近原则
在这样的架构下,会延伸出这样的几个概念:
当观众人数不太多的时候,例如总共只有1000人,那么是选择让某一台服务器服务这1000人,还是3台服务器分担1000人,还是2台?机器也会有新旧之分,老机器只能抗800数量,那要怎么来分配呢?等等问题。这里就需要有一个策略来做资源的分配。这个策略叫做:负载均衡。
因为观众看到的数据都是一样的,所以呢,数据会在服务器1、2、3上都存储一份。这个概念叫做:CDN缓存。
当分配到服务器1的第一个观众进入时,服务器1是没有存储数据的,它会向服务器-0获取数据,这个过程叫做:回源;相应的,服务器-0被称为:源站;观众请求的数据如果由CDN缓存提供,叫做缓存命中,所有用户请求的缓存命中比例叫做缓存命中率,它是衡量CDN质量的关键指标。
一名新进入的观众会被分配到哪一台服务器上呢?理论上,这台服务器距离用户的网络链路越短、不跨网,数据的传输的稳定性就越好,这个叫做:就近原则。
跨地区、多运营商覆盖的CDN
由于就近原则的存在,为了满足全国甚至全世界不同地方的人,那我们就需要把服务器分布在不同的地区。又由于不同的网络运营商之间的网络传输会有稳定性问题,那么就需要在不同的网络运营商里也放置服务器,于是,一个CDN网络就成型了:
传统直播一般是基于CDN网络进行分发,可支持大规模并发(并发数取决于CDN网络容量)。与传统CDN的大文件,小文件分发不同,由于直播分布区域分散,一般除了提供播放端的下行分发网络外,还提供上行主播推流汇聚网络。只有 一些直播内容资源集中的业务方,会要求直播CDN直接回自己的源站,如电视台。
上行汇聚
目前传统直播 CDN 上行一般使用 RTMP 协议,当然也有一些使用 UDP(UDP 方式由于需要 SDK 配合,目前行业内有人在做,但是需要绑定 SDK)。另外国外还有使用 http-ts 的方式进行推流的,可参见 nginx-rtmp 项目大神开源的 nginx-ts-module。当然,目前使用这种方式,关键问题还是在于端的支持问题,而该开源项目目前只支持 HLS 和 Dash 的播放。
除了主播推流以外,还有一种方式即从汇聚点到业务方源站去拉流的方式
下行分发
目前下行分发一般使用的协议,rtmp,http-flv,hls 三种协议。这三种协议的优劣,网上已经有很多文章了,一般从终端兼容性,延迟,首屏几个维度去考虑,这里就不在进行比较。
rtmp 和 http-flv
由于 rtmp 协议在发送数据前交互次数较多,比较追求首屏的直播平台一般都会选择 http-flv 协议作为下行分发协议,线上环境测试效果平均会增加 100-200 ms 左右的时间,网络越差,这个值越大。
rtmp 和 http-flv 的延迟可以做到 3s 以内,但是由于网络环境的复杂,过低的延迟会导致卡顿率的提升,所以一般 CDN 会用户接入时,给用户多发几秒钟的数据(一般是 5-8s),填充播放端缓冲区,来抗网络端的抖动。细节技术会在后面的文章中介绍
hls
hls 对 Android 端和 IOS 端支持较好,并且对 P2P 的支持也较好,一般对延迟要求不高的直播平台(如体育赛事)会选用这个协议。
hls 的延迟一般和切片大小有关,一般切片是 6-8s 一个片,这个大小对一般主播推流 GOP 适配最好。过高会导致延迟加大,过低,可能切片里就没有关键帧。一般 m3u8 文件里会有 3 个 ts 文件,播放器会在下完两个片以后,开始播放,并且同时下第三个片。因此一般 hls 的时延在 15s 左右。
当然如果用户调小 GOP(1s),CDN 端将切片方式配置为按 GOP 切片的方式,HLS 实际也可以做到 5s 以内延迟的。当然坏处就是会导致卡顿率变高
拉流播放器播放
视音频编解码一般分为两种,一种是硬编实现,一种是软编实现。这两种方式各有优缺点,硬编性能好,但是需要对兼容性进行相应处理;软编兼容性好,可以进行一些参数设置,但是软编一般性能较差,引入相关的编解码库往往会增大app的整体体积,而且还需要写相应的jni接口。
先介绍几个和视音频相关的类,通过这几个类的组合使用,其实是能变换出许多视音频处理的相关功能
MediaMetadataRetriever::用来获取视频的相关信息,例如视频宽高、时长、旋转角度、码率等等。
MediaExtractor::视音频分离器,将一些格式的视频分离出视频轨道和音频轨道。
MediaCodec:视音频相应的编解码类。
MediaMuxer:视音频合成器,将视频和音频合成相应的格式。
MediaFormat:视音频相应的格式信息。
MediaCodec.BufferInfo:存放ByteBuffer相应信息的类。
MediaCrypto:视音频加密解密处理的类。
MediaCodecInfo:视音频编解码相关信息的类。
MediaFormat和MediaCodec.BufferInfo是串起上面几个类的桥梁,上面几个视音频处理的类通过这两个桥梁建立起联系,从而变化出相应的功能
MediaMetadataRetriever
MediaMetadataRetriever用来获取视音频的相关信息,MediaMetadataRetriever的使用十分简单,传入相应的文件路径创建MediaMetadataRetriever,之后便可以得到视频的相关参数。
MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
metadataRetriever.setDataSource(file.getAbsolutePath());
String widthString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
if(!TextUtils.isEmpty(widthString)) {
width = Integer.valueOf(widthString);
}
String heightString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
if(!TextUtils.isEmpty(heightString)) {
height = Integer.valueOf(heightString);
}
String durationString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
if(!TextUtils.isEmpty(durationString)) {
duration = Long.valueOf(durationString);
}
String bitrateString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE);
if(!TextUtils.isEmpty(bitrateString)) {
bitrate = Integer.valueOf(bitrateString);
}
String degreeStr = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
if (!TextUtils.isEmpty(degreeStr)) {
degree = Integer.valueOf(degreeStr);
}
metadataRetriever.release();
MediaExtractor
MediaExtractor用来对视音频进行分离,对文件中的视频音频轨道进行分离能做大量的事情,比如说要写一个播放器,那么首先的第一个步骤是分离出视频音频轨道,然后进行相应的处理。
MediaExtractor的创建和MediaMetadataRetriever一样十分简单,只需要传入相应的文件路径。通过getTrackCount()可以得到相应的轨道数量,一般情况下视音频轨道都有,有些时候可能只有视频,有些时候可能只有音频。轨道的序号从0开始,通过getTrackFormat(int index)方法可以得到相应的MediaFormat,而通过MediaFormat可以判断出轨道是视频还是音频。通过selectTrack(int index)方法选择相应序号的轨道。
public static MediaExtractor createExtractor(String path) throws IOException {
MediaExtractor extractor;
File inputFile = new File(path); // must be an absolute path
if (!inputFile.canRead()) {
throw new FileNotFoundException("Unable to read " + inputFile);
}
extractor = new MediaExtractor();
extractor.setDataSource(inputFile.toString());
return extractor;
}
public static String getMimeTypeFor(MediaFormat format) {
return format.getString(MediaFormat.KEY_MIME);
}
public static int getAndSelectVideoTrackIndex(MediaExtractor extractor) {
for (int index = 0; index < extractor.getTrackCount(); ++index) {
if (isVideoFormat(extractor.getTrackFormat(index))) {
extractor.selectTrack(index);
return index;
}
}
return -1;
}
public static int getAndSelectAudioTrackIndex(MediaExtractor extractor) {
for (int index = 0; index < extractor.getTrackCount(); ++index) {
if (isAudioFormat(extractor.getTrackFormat(index))) {
extractor.selectTrack(index);
return index;
}
}
return -1;
}
public static boolean isVideoFormat(MediaFormat format) {
return getMimeTypeFor(format).startsWith("video/");
}
public static boolean isAudioFormat(MediaFormat format) {
return getMimeTypeFor(format).startsWith("audio/");
}
选择好一个轨道后,便可以通过相应方法提取出相应轨道的数据。extractor.seekTo(startTime, SEEK_TO_PREVIOUS_SYNC)方法可以直接跳转到开始解析的位置。extractor.readSampleData(byteBuffer, 0)方法则可以将数据解析到byteBuffer中。extractor.advance()方法则将解析位置进行前移,准备下一次解析。
下面是MediaExtractor一般的使用方法。
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(...);
int numTracks = extractor.getTrackCount();
for (int i = 0; i < numTracks; ++i) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (weAreInterestedInThisTrack) {
extractor.selectTrack(i);
}
}
ByteBuffer inputBuffer = ByteBuffer.allocate(...)
while (extractor.readSampleData(inputBuffer, ...) >= 0) {
int trackIndex = extractor.getSampleTrackIndex();
long presentationTimeUs = extractor.getSampleTime();
...
extractor.advance();
}
extractor.release();
extractor = null;
MediaCodec
MediaCodec是Android视音频里面最为重要的类,它主要实现的功能是对视音频进行编解码处理。在编码方面,可以对采集的视音频数据进行编码处理,这样的话可以对数据进行压缩,从而实现以较少的数据量存储视音频信息。在解码方面,可以解码相应格式的视音频数据,从而得到原始的可以渲染的数据,从而实现视音频的播放。
一般场景下音频使用的是AAC-LC的格式,而视频使用的是H264格式。这两种格式在MediaCodec支持的版本(Api 16)也都得到了很好的支持。在直播过程中,先采集视频和音频数据,然后将原始的数据塞给编码器进行硬编,然后得到相应的编码后的AAC-LC和H264数据。
在Android系统中,MediaCodec支持的格式有限,在使用MediaCodec之前需要对硬编类型的支持进行检测,如果MediaCodec支持再进行使用。
1、检查
在使用硬编编码器之前需要对编码器支持的格式进行检查,在Android中可以使用MediaCodecInfo这个类来获取系统对视音频硬编的支持情况。
下面的代码是判断MediaCodec是否支持某个MIME:
private static MediaCodecInfo selectCodec(String mimeType) {
int numCodecs = MediaCodecList.getCodecCount();
for (int i = 0; i < numCodecs; i++) {
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
if (!codecInfo.isEncoder()) {
continue;
}
String[] types = codecInfo.getSupportedTypes();
for (int j = 0; j < types.length; j++) {
if (types[j].equalsIgnoreCase(mimeType)) {
return codecInfo;
}
}
}
return null;
}
根据之前的讲述,在Android系统中有着不同的颜色格式,有着各种类型的YUV颜色格式和RGB颜色格式。在摄像头采集的文章中已经讲述,需要设置摄像头采集的图像颜色格式,一般来说设置为ImageFormat.NV21,之后在摄像头PreView的回调中得到相应的图像数据。
在Android系统中不同手机中的编码器支持着不同的颜色格式,一般情况下并不直接支持NV21的格式,这时候需要将NV21格式转换成为编码器支持的颜色格式。在摄像头采集的文章中已经详细讲述YUV图像格式和相应的存储规则,YUV图像格式的转换可以使用LibYuv。
这里说一下MediaCodec支持的图像格式。一般来说Android MediaCodec支持如下几种格式:
/**
* Returns true if this is a color format that this test code understands (i.e. we know how
* to read and generate frames in this format).
*/
private static boolean isRecognizedFormat(int colorFormat) {
switch (colorFormat) {
// these are the formats we know how to handle for this test
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
return true;
default:
return false;
}
}
上面大致统计了Android各种手机MediaCodec支持的各种颜色格式,上面5个类型是比较常用的类型。
另外MediaCodec支持Surface的方式输入和输出,当编码的时候只需要在Surface上进行绘制就可以输入到编码器,而解码的时候可以将解码图像直接输出到Surface上,使用起来相当方便,需要在Api 18或以上。
2、创建
当需要使用MediaCodec的时候,首先需要根据视音频的类型创建相应的MediaCodec。在直播项目中视频使用了H264,而音频使用了AAC-LC。在Android中创建直播的音频编码器需要传入相应的MIME,AAC-LC对应的是audio/mp4a-latm,而H264对应的是video/avc。如下的代码展示了两个编码器的创建,其中视频编码器的输入设置成为了Surface的方式。
//Audio
public static MediaCodec getAudioMediaCodec() throws IOException {
int size = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
MediaFormat format = MediaFormat.createAudioFormat("audio/mp4a-latm", 44100, 1);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_BIT_RATE, 64 * 1000);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, size);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
return mediaCodec;
}
//Video
public static MediaCodec getVideoMediaCodec() throws IOException {
int videoWidth = getVideoSize(1280);
int videoHeight = getVideoSize(720);
MediaFormat format = MediaFormat.createVideoFormat("video/avc", videoWidth, videoHeight);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, 1300* 1000);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
format.setInteger(MediaFormat.KEY_BITRATE_MODE,MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
format.setInteger(MediaFormat.KEY_COMPLEXITY,MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
MediaCodec mediaCodec = MediaCodec.createEncoderByType("video/avc");
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
return mediaCodec;
}
// We avoid the device-specific limitations on width and height by using values that
// are multiples of 16, which all tested devices seem to be able to handle.
public static int getVideoSize(int size) {
int multiple = (int) Math.ceil(size/16.0);
return multiple*16;
}