ios 知识点iOS开发杂货铺iOS 直播视频

音视频 直播 滤镜

2018-01-15  本文已影响326人  勇敢的_心_

视频

视频实质:
纯粹的视频(不包括音频)实质上就是一组帧图片,经过视频编码成为视频(video)文件再把音频(audio)文件有些还有字幕文件组装在一起成为我们看到的视频(movie)文件。1秒内出现的图片数就是帧率,图片间隔越小画面就越流畅,所以帧率越高效果就越好,需要的存储空间也就越多。

1049769-1d1203b99b1eaa20.png

视频编码:
因为不进行编码的视频数据量非常大,会造成存储和传输上的困难,所以视频文件都需要在录制完成后进行编码。视频编码主要从两个维度压缩数据。
1、单张图像某一区域相邻像素相似,比如一片红色只记录红色色值和区域,不用记录这个区域的每一个像素点。
2、相邻图像之间内容相似,因为相邻两帧要制造连续的效果,所以两帧之间的内容一般非常接近。目前主流的视频编码技术都是用图像编码方法对第一帧进行编码,然后用某种方式描述接下来的帧相对于附近的帧有什么区别。

视频格式:
MP4、MOV、AVI、RMVB这些播放格式其实都是封装格式,除了RMVB比较特殊外,其他格式内封装的视频编码格式都是H264,H264以高压缩率闻名于世,压缩效率比MEPG-2提升一倍多,但是世上没有两全其美的事,H264的解码难度提高了3倍多。

视频码率:
视频文件的大小除以是视频的时长定义为码率。

码率和分辨率跟视频质量的关系:
码率可以理解为取样率,单位时间内取样率越大,精度就越高,同时体积也越大。
当视频没有经过编码时,如果分辨率越高,那么视频图像的细节越清晰。
但如果视频经过编码,被限制在一定码率内,编码器就必须舍弃掉一部分细节。
所以分辨率和码率都同清晰度有关。

软解码和硬解码:
对H264的视频解码给CPU造成了很大负担,所以手机工程师把这部分工作交给了更善于进行处理简单工作但是数据量较大的GPU。
GPU解码就是所谓的硬解码
CPU解码就是软解码。
iOS提供的播放器类使用的是硬解码,所以视频播放对CPU不会有很大的压力,但是支持的播放格式比较单一,一般就是MP4、MOV、M4V这几个。

视频压缩原理

1、压缩的方向
数字化后的视频信号具有很大的数据冗余,压缩的本质就是去掉这些冗余。

2、变换
空间域描述的图像相关性不太明显,需要变换到频率域。常用的正交变换有离散傅里叶变换,离散余弦变换等等。数字视频压缩过程中应用广泛的是离散余弦变换。

3.H.264格式

音频压缩原理

数字音频压缩编码在保证信号在听觉方面不产生失真的前提下,对音频数据信号进行尽可能大的压缩。数字音频压缩编码采取去除声音信号中冗余成分的方法来实现。所谓冗余成分指的是音频中不能被人耳感知到的信号,它们对确定声音的音色,音调等信息没有任何的帮助。
冗余信号包含人耳听觉范围外的音频信号以及被掩蔽掉的音频信号等。
人耳听觉的掩蔽效应:当一个强音信号与一个弱音信号同时存在时,弱音信号将被强音信号所掩蔽而听不见,这样弱音信号就可以视为冗余信号而不用传送。

频谱掩蔽效应
一个频率的声音能量小于某个阈值之后,人耳就会听不到,这个阈值称为最小可闻阈。当有另外能量较大的声音出现的时候,该声音频率附近的阈值会提高很多,即所谓的掩蔽效应。

时域掩蔽效应
当强音信号和弱音信号同时出现时,还存在时域掩蔽效应。即两者发生时间很接近的时候,也会发生掩蔽效应。时域掩蔽过程曲线如图所示,分为前掩蔽、同时掩蔽和后掩蔽三部分。

数据大小=采样频率采样位数声道*秒数/8。
采样定理表明采样频率必须大于被采样信号带宽的两倍,另外一种等同的说法是奈奎斯特频率必须大于被采样信号的带宽。如果信号的带宽是 100Hz,那么为了避免混叠现象采样频率必须大于200Hz。换句话说就是采样频率必须至少是信号中最大频率分量频率的两倍,否则就不能从信号采样中恢复原始信号。

iOS音频

HTTP Live Streaming
HLS简介
HTTP Live Streaming(缩写是 HLS)是一个由苹果公司提出的基于HTTP的流媒体网络传输协议。它的工作原理是把整个流分成一个个小的基于HTTP的文件来下载,每次只下载一些。

当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。支持的视频流编码为H.264。我们在视频网站上看到的M3U8后缀的播放链接就是使用HLS协议的视频。

HLS优点
1、看完一段缓存一段,防止只看一段视频但是把整个视频文件都缓存下来的用户,减少服务器压力和节省流量。
2、根据用户网速切换不同的码率,兼顾流程性和清晰度。

HLS支持情况
iOS 3.0及之后的版本
Android 3.0及之后的版本
HTML5。

终端播放格式的选取
Android由于3.0之后才支持HLS,所以Android2.3只能用MP4。
Android3.0及之后支持HLS。可以用m3u8、mp4格式
iOS支持HLS,但不支持flash。可以用m3u8、mp4格式
支持HTML5的浏览器 可以用m3u8。
不支持HTML5的浏览器只能用flash播放swf。
由于以上原因,目前无法实现一个播放地址在所有的平台都通用。

iOS视频播放:
iOS提供MPMoviePlayerController类进行播放,支持流媒体和文件播放。视频内容会渲染到他的View上,可以放在你想放的任何地方,用起来比较方便。这个类设计上不合理的是视频播放状态和视频加载状态都是通过Notification通知的,而不是通过block或者delegate。

iOS视频录制:
同拍照一样视频录制功能有两种实现方式
1、UIImagePickerViewController
2、AVFoundation。

这里只讨论AVFoundation框架,这个框架是苹果提供的底层多媒体框架,用于音视频采集、音视频解码、视频编辑等,多媒体基本上都依赖AVFoundation框架。

视频录制和拍照需要做的工作差不多,主要有以下5步:
1、创建会话AVCaptureSession,用于控制input到output的流向。
2、获取设备AVCaptureDevice,摄像头用于视频采集,话筒用于音频采集。
3、创建输入设备AVCaptureDeviceInput,将设备绑定到input口中,并添加到session上
4、创建输出AVCaptureOutput,可以输出到文件和屏幕上。 AVCaptureMovieFileOutput 输出一个电影文件 AVCaptureVideoDataOutput 输出处理视频帧,用于显示正在录制的视频 AVCaptureAudioDataOutput 输出音频数据
5、音视频合成到一个文件中

iOS对视频实时处理:
如果需要对视频进行实时处理(当然需要否则看不到正在录制的内容),则需要直接对相机缓冲区(camera buffer)中的视频流进行处理。
1、定义一个视频数据输出(AVCaptureVideoDataOutput), 并将其添加到session上。
2、设置接受的controller作为视频数据输出缓冲区(sample buffer)的代理。
3、实现代理方法
-(void)captureOutput:(AVCaptureOutput )captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection )connection

当数据缓冲区(data buffer)一有数据时,AVFoundation就调用该方法。在该代理方法中,我们可以获取视频帧、处理视频帧、显示视频帧。实时滤镜就是在这里进行处理的。在这个方法中将缓冲区中的视频数据(就是帧图片)输出到要显示的layer上。

视频编码与解码
移动端编码无外乎两种:
软编码,利用CPU来对视频做编码和解码的,但效率不高.(大部分移动端通过FFMpeg来实现软编码。)
硬编码,利用GPU或者专用处理器来对视频做编码和解码。iOS 8.0之后开放的Video ToolBox框架就是来实现硬件的编码和解码的.

视频数据硬编码
视频压缩编码通过Video Toolbox框架下的VTCompressionSession完成。Video ToolBox 是一个基于 CoreMedia,CoreVideo,CoreFoundation 框架的 C 语言 API,来处理硬件的编码和解码,在iOS 8.0后,苹果将该框架引入iOS系统,苹果在iOS 8.0系统之前,没有开放系统的硬件编码解码功能。
由于Video ToolBox 提供的是 C 的API,所以编码时会用到一些swift的指针操作。

定义创建配置CompressionSession:
private var session:VTCompressionSessionRef?
// 将
VTCompressionSessionCreate(
    kCFAllocatorDefault,
    480, // encode height
    640,// encode width
    kCMVideoCodecType_H264, //encode type,h.264
    nil,
    attributes,  
    nil,
    callback,
    unsafeBitCast(self, UnsafeMutablePointer<Void>.self),
    &session)
// 设置编码的属性
VTSessionSetProperties(session!, properties)
VTCompressionSessionPrepareToEncodeFrames(session!)
VTCompressionSessionCreate 的encode height, encode width设置编码输出的h.264文件的宽高,单位px(像素)。

编码类型主要用的就是kCMVideoCodecType_H264(h.264),其他的类型还有h.263等,好像最新的 iphone 6s 已经支持 h.265的编码,但还没有放出接口。

硬解码和硬编码使用方法
1.创建AVAssetReader,将H264码流转换成解码前的CMSampleBuffer。
(1)提取sps和pps生成format descriptio
(2)提取视频图像数据生成CMBlockBuffer。
(3)根据需要,生成CMTime信息。
2.创建AVAssetWriter,设置输出及压缩属性。

iOS视频直播~推流、拉流原理

HLS协议:
简单讲就是把整个流分成一个个小的,基于 HTTP 的文件来下载,每次只下载一些,前面提到了用于 H5 播放直播视频时引入的一个 .m3u8 的文件,这个文件就是基于 HLS 协议,存放视频流元数据的文件。

每一个 .m3u8 文件,分别对应若干个 ts 文件,这些 ts 文件才是真正存放视频的数据,m3u8 文件只是存放了一些 ts 文件的配置信息和相关路径,当视频播放时,.m3u8 是动态改变的,video 标签会解析这个文件,并找到对应的 ts 文件来播放,所以一般为了加快速度,.m3u8 放在 web 服务器上,ts 文件放在 cdn 上。

.m3u8 文件,其实就是以 UTF-8 编码的 m3u 文件,这个文件本身不能播放,只是存放了播放信息的文本文件:

#EXTM3U                 m3u文件头
#EXT-X-MEDIA-SEQUENCE   第一个TS分片的序列号
#EXT-X-TARGETDURATION   每个分片TS的最大的时长
#EXT-X-ALLOW-CACHE      是否允许cache
#EXT-X-ENDLIST          m3u8文件结束符
#EXTINF                 指定每个媒体段(ts)的持续时间(秒),仅对其后面的URI有效mystream-12.ts

HLS 的请求流程是:
1.http 请求 m3u8 的 url。
2.服务端返回一个 m3u8 的播放列表,这个播放列表是实时更新的,一般一次给出5段数据的 url。
3.客户端解析 m3u8 的播放列表,再按序请求每一段的 url,获取 ts 数据流。

关于HLS延迟原因:
hls 协议是将直播流分成一段一段的小段视频去下载播放的,所以假设列表里面的包含5个 ts 文件,每个 TS 文件包含5秒的视频内容,那么整体的延迟就是25秒。因为当你看到这些视频时,主播已经将视频录制好上传上去了,所以时这样产生的延迟。当然可以缩短列表的长度和单个 ts 文件的大小来降低延迟,极致来说可以缩减列表长度为1,并且 ts 的时长为1s,但是这样会造成请求次数增加,增大服务器压力,当网速慢时回造成更多的缓冲,所以苹果官方推荐的ts时长时10s,所以这样就会大改有30s的延迟;

数据采集原理:
下面将利用 ios 上的摄像头,进行音视频的数据采集,主要分为以下几个步骤:

RTMP介绍:
Real Time Messaging Protocol(简称 RTMP)是 Macromedia 开发的一套视频直播协议。和 HLS 一样都可以应用于视频直播,区别是 RTMP 基于 flash 无法在 ios 的浏览器里播放,但是实时性比 HLS 要好。所以一般使用这种协议来上传视频流,也就是视频流推送到服务器。
对比:

推流:
推流,就是将我们已经编码好的音视频数据发往视频流服务器中,一般常用的是使用 rtmp 推流,可以使用第三方库 librtmp-iOS 进行推流,librtmp 封装了一些核心的 api 供使用者调用,如果觉得麻烦,可以使用现成的 ios 视频推流sdk,也是基于 rtmp 的。具体说下:
也就是对编码好的音视频数据推到服务器上,这里我们又分为两类推流模式:手机端推流,服务器本地推流。就拿我上一家公司的电视直播来说,视频源是来自电视台的,需要通过ffmpeg命令来进行个推流,那么推流协议的话这里又分为了:HLS推流和rtmp推流,这里的取舍主要涉及到了是否需要及其实时的直播问题,也就是延迟20 30s是否接受,当然电视直播并不是主播实时互动,所以不需要使用实时流媒体协议的rtmp,所以通过ffmpeg -loglevel 这么一个命令将电视台给的视频进行各像nginx服务器的一个推流,那么我们就可以通过nginx服务器给的链接,配合我的第三方的直播框架,就可以实现个直播,这个是服务器本地的HLS协议的一个推流。当然如果我们要做一个没有延迟的比如实现各主播互动的一个直播,那么就是iOS客户端用rtmp协议的一个往nginx服务器的一个推流了。在iOS设备上进行各推流的话,是通过AVCaptureSession这么一个捕捉会话,指定两个AVCaptureDevice 也就是iOS的摄像头和麦克风,获取个原始视频和音频,然后需要进行个H.264的视频编码和AAC的音频编码,再将编码后的数据整合成一个音视频包,通过rmtp推送到nginx服务器。这里这些步骤,我们可以通过各第三方集成好的推流工具进行推流,这个工具有librtmp,和腾讯的GDLiveStreaming进行个推流。

直播

304825-54974199408c0cc1.png

流媒体(直播需要用到流媒体)

视频编码框架

流媒体服务器

数据分发

直播协议

iOS视频边下边播--缓存播放数据流

使用本地代理服务器的方式,原理很简单,但是缺点也很明显,需要自己写一个本地代理服务器或者使用第三方库httpSever。如果使用httpSever作为本地代理服务器,如果只缓存一个视频是没有问题的,如果缓存多个视频互相切换,本地代理服务器提供的数据很不稳定,crash概率非常大。

这里我采用ios7以后系统自带的方法实现视频边下边播,这里的边下边播不是单独开一个子线程去下载,而是把视频播放的数据给保存到本地。简而言之,就是使用一遍的流量,既播放了视频,也保存了视频。
用到的框架:<AVFoundation/AVFoundation.h>
用到的播放器:AVplayer
先说一下avplayer自身的播放原理,当我们给播放器设置好url等一些参数后,播放器就会向url所在的服务器发送请求(请求参数有两个值,一个是offset偏移量,另一个是length长度,其实就相当于NSRange一样),服务器就根据range参数给播放器返回数据。

产品需求:
1.支持正常播放器的一切功能,包括暂停、播放和拖拽
2.如果视频加载完成且完整,将视频文件保存到本地cache,下一次播放本地cache中的视频,不再请求网络数据
3.如果视频没有加载完(半路关闭或者拖拽)就不用保存到本地cache

实现方案:
1.需要在视频播放器和服务器之间添加一层类似代理的机制,视频播放器不再直接访问服务器,而是访问代理对象,代理对象去访问服务器获得数据,之后返回给视频播放器,同时代理对象根据一定的策略缓存数据。
2.AVURLAsset中的resourceLoader可以实现这个机制,resourceLoader的delegate就是上述的代理对象。
3.视频播放器在开始播放之前首先检测是本地cache中是否有此视频,如果没有才通过代理获得数据,如果有,则直接播放本地cache中的视频即可。

代理对象需要实现的功能
1.接收视频播放器的请求,并根据请求的range向服务器请求本地没有获得的数据
2.缓存向服务器请求回的数据到本地
3.如果向服务器的请求出现错误,需要通知给视频播放器,以便视频播放器对用户进行提示

流程图:


971366-0a9b11be2df75aaa.png

视频播放器处理流程
1.当开始播放视频时,通过视频url判断本地cache中是否已经缓存当前视频,如果有,则直接播放本地cache中视频
2.如果本地cache中没有视频,则视频播放器向代理请求数据
3.加载视频时展示正在加载的提示(菊花转)
4.如果可以正常播放视频,则去掉加载提示,播放视频,如果加载失败,去掉加载提示并显示失败提示
5.在播放过程中如果由于网络过慢或拖拽原因导致没有播放数据时,要展示加载提示,跳转到第4步

代理对象处理流程
1.当视频播放器向代理请求dataRequest时,判断代理是否已经向服务器发起了请求,如果没有,则发起下载整个视频文件的请求
2.如果代理已经和服务器建立链接,则判断当前的dataRequest请求的offset是否大于当前已经缓存的文件的offset,如果大于则取消当前与服务器的请求,并从offset开始到文件尾向服务器发起请求(此时应该是由于播放器向后拖拽,并且超过了已缓存的数据时才会出现)
3.如果当前的dataRequest请求的offset小于已经缓存的文件的offset,同时大于代理向服务器请求的range的offset,说明有一部分已经缓存的数据可以传给播放器,则将这部分数据返回给播放器(此时应该是由于播放器向前拖拽,请求的数据已经缓存过才会出现)
4.如果当前的dataRequest请求的offset小于代理向服务器请求的range的offset,则取消当前与服务器的请求,并从offset开始到文件尾向服务器发起请求(此时应该是由于播放器向前拖拽,并且超过了已缓存的数据时才会出现)
5.只要代理重新向服务器发起请求,就会导致缓存的数据不连续,则加载结束后不用将缓存的数据放入本地cache
6.如果代理和服务器的链接超时,重试一次,如果还是错误则通知播放器网络错误
7.如果服务器返回其他错误,则代理通知播放器网络错误

resourceLoader的难点处理

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
    [self.pendingRequests addObject:loadingRequest];
    [self dealWithLoadingRequest:loadingRequest];

    return YES;
}

播放器发出的数据请求从这里开始,我们保存从这里发出的所有请求存放到数组,自己来处理这些请求,当一个请求完成后,对请求发出finishLoading消息,并从数组中移除。正常状态下,当播放器发出下一个请求的时候,会把上一个请求给finish。

下面这个方法发出的请求说明播放器自己关闭了这个请求,我们不需要再对这个请求进行处理,系统每次结束一个旧的请求,便必然会发出一个或多个新的请求,除了播放器已经获得整个视频完整的数据,这时候就不会再发起请求。

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
    [self.pendingRequests removeObject:loadingRequest];

}

下面这个方法是对播放器发出的请求进行填充数据

- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest
{
    long long startOffset = dataRequest.requestedOffset;

    if (dataRequest.currentOffset != 0) {
        startOffset = dataRequest.currentOffset;
    }

    if ((self.task.offset +self.task.downLoadingOffset) < startOffset)
    {
        //NSLog(@"NO DATA FOR REQUEST");
        return NO;
    }

    if (startOffset < self.task.offset) {
        return NO;
    }

    NSData *filedata = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:_videoPath] options:NSDataReadingMappedIfSafe error:nil];

    // This is the total data we have from startOffset to whatever has been downloaded so far
    NSUInteger unreadBytes = self.task.downLoadingOffset - ((NSInteger)startOffset - self.task.offset);

    // Respond with whatever is available if we can't satisfy the request fully yet
    NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);


    [dataRequest respondWithData:[filedata subdataWithRange:NSMakeRange((NSUInteger)startOffset- self.task.offset, (NSUInteger)numberOfBytesToRespondWith)]];



    long long endOffset = startOffset + dataRequest.requestedLength;
    BOOL didRespondFully = (self.task.offset + self.task.downLoadingOffset) >= endOffset;

    return didRespondFully;


}

这是对存放所有的请求的数组进行处理

- (void)processPendingRequests
{
    NSMutableArray *requestsCompleted = [NSMutableArray array];  //请求完成的数组
    //每次下载一块数据都是一次请求,把这些请求放到数组,遍历数组
    for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests)
    {
        [self fillInContentInformation:loadingRequest.contentInformationRequest]; //对每次请求加上长度,文件类型等信息

        BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest]; //判断此次请求的数据是否处理完全

        if (didRespondCompletely) {

            [requestsCompleted addObject:loadingRequest];  //如果完整,把此次请求放进 请求完成的数组
            [loadingRequest finishLoading];

        }
    }

    [self.pendingRequests removeObjectsInArray:requestsCompleted];   //在所有请求的数组中移除已经完成的

}

resourceLoader的难点基本上就是上面这点了,说到播放器,下面便顺便讲下AVPlayer的难点。

难点:对播放器状态的捕获

举个简单的例子,视频总长度60分,现在缓冲的数据才10分钟,然后拖动到20分钟的位置进行播放,在网速较慢的时候,视频从当前位置开始播放,必然会出现一段时间的卡顿,为了有一个更好的用户体验,在卡顿的时候,我们需要加一个菊花转的状态,现在问题就来了。
在拖动到未缓冲区域内,是否需要加菊花转,如果加,要显示多久再消失,而且如果在网速很慢的时候,播放器如果等了太久,哪怕最后有数据了,播放器也已经“死”了,它自己无法恢复播放,这个时候需要我们人为的去恢复播放,如果恢复播放不成功,那么过一段时间需要再次恢复播放,是否恢复播放成功,这里也需要捕获其状态。所以,如果要有一个好的用户体验,我们需要时时知道播放器的状态。

有两个状态需要捕获,一个是正在缓冲,一个是正在播放,监听播放的“playbackBufferEmpty”属性就可以捕获正在缓冲状态,播放器的时间监听器则可以捕获正在播放状态,我的demo中一共有4个状态:

typedef NS_ENUM(NSInteger, TBPlayerState) {
    TBPlayerStateBuffering = 1,
    TBPlayerStatePlaying   = 2,
    TBPlayerStateStopped   = 3,
    TBPlayerStatePause     = 4
};

这样可以对播放器更好的把握和处理了。
然后说一说在缓冲时候的处理,以及缓冲后多久去播放,处理方法:
进入缓冲状态后,缓冲2秒后去手动播放,如果播放不成功(缓冲的数据太少,还不足以播放),那就再缓冲2秒再次播放,如此循环,看详细代码:

- (void)bufferingSomeSecond
{
    // playbackBufferEmpty会反复进入,因此在bufferingOneSecond延时播放执行完之前再调用bufferingSomeSecond都忽略
    static BOOL isBuffering = NO;
    if (isBuffering) {
        return;
    }
    isBuffering = YES;

    // 需要先暂停一小会之后再播放,否则网络状况不好的时候时间在走,声音播放不出来
    [self.player pause];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        // 如果此时用户已经暂停了,则不再需要开启播放了
        if (self.isPauseByUser) {
            isBuffering = NO;
            return;
        }

        [self.player play];
        // 如果执行了play还是没有播放则说明还没有缓存好,则再次缓存一段时间
        isBuffering = NO;
        if (!self.currentPlayerItem.isPlaybackLikelyToKeepUp) {
            [self bufferingSomeSecond];
        }
    });
}

利用GPUImage处理直播过程中美颜的流程

采集视频 => 获取每一帧图片 => 滤镜处理 => GPUImageView展示

GPUImage处理画面原理
GPUImage采用链式方式来处理画面,通过addTarget:方法为链条添加每个环节的对象,处理完一个target,就会把上一个环节处理好的图像数据传递下一个target去处理,称为GPUImage处理链。

零碎的知识

FLV(Flash Video)是Adobe公司设计开发的一种流行的流媒体格式,由于其视频文件体积轻巧、封装简单等特点,使其很适合在互联网上进行应用。
RTSP:实时流传输协议,是TCP/IP协议体系中的一个应用层协议;

M4A:.m4a是MPEG-4 音频标准的文件的扩展名,Apple在iTunes以及 iPod中使用“.m4a”以区别MPEG4的视频和音频文件;

音视频同步:时间戳,时间戳即为一帧的采集时间,音视频采取同一个参考时间,给每个帧打上时间戳。

rtmp发送音视频:xcode中编译librtmp库,遵循rtmp协议,将数据发送到指定服务器;

AudioToolbox.framework:提供CoreAudio的中高级别的API服务,处理电话和其他高优先级语音处理而导致的中断和恢复操作等;

AudioUnit.framework:提供DSP数字信号处理相关的插件,包括编解码,混音,音频均衡等;

AVFoundation.framework:提供一个精简的音乐播放类,可以播放所有IOS支持的音频;

OpenAL.framework:提供3D音效播放;

直播平台的高并发架构设计

需求:
用户那边提的需求就是推流端不能卡,画质要好,不能太烫

低延时:
低延时的操作大部分来自端的配合,服务端只要是做好缓存,保证这个数据是连贯的。如果要丢数据的话,把关键帧保留好,丢GOP中间那些PB帧,主要是在端上会收到。

首屏时间:
就是用户点开就要看,以前那些开源架构就是rtmp server,它是做不到一点开就能看的,现在一些开源的国内资源写得也比较好了,可以看到。我们是自己开发的,能保存之前的关键帧的信息,用户一点开就能看,这个就是很细节的东西了。如果这个做不好的话,会黑屏、绿屏,或者是半天看不着图像。

不能卡,不能延迟太高:
要满足这些需求,我们需要做好多分辨率的适配,保证好流畅性,保证好我们追赶的策略不会出现任何异常。所以这三个端很多是相互耦合的,像推流和分发在一起,要保障好用户的流畅性和画质,分发和播放器在一起要保证好低延时和播放的流畅。

一般的流媒体的数据走向、各种请求。但是其中有一些坑我可以跟大家重点说一下,首先看一下直播发起流程,这肯定是由应用向自己的服务端去请求一个推流地址,这个推流地址他就用来向我们的流媒体服务器推,然后我们给它鉴权。

鉴权之后,它可以在参数里选择是不是要录像。如果需要录像截图,或者需要HLS的分发,我们都可以帮他做,做完之后存到我们的存储里,这也是后面会提到的,我们各个业务之间在做隔离、分不同的优先级,这种后端的多媒体的处理尽量都会依赖别的服务,然后就是正常的结束流程。

一般互联网公司做云服务都怎么做?都是给回调,如果这个推流结束了,我来回调业务方,让业务方知道我结束了,你可以做你的逻辑了。

但实际操作中我们遇到了问题,就是业务方的服务器没那么可靠,我们可能过去时间特别久,有延时,有丢,或者他们的服务稳定性我们也确认不了,这其实就是一个双方的耦合了。而且它的服务器,由于是我们来调,它的鉴权功能没有办法做得很复杂,他自己的服务器也存在安全漏洞。如果有人来攻击他的话,他的整个业务流程的状态全是乱的。

在试了几家客户之后,我们就改成另外一种方式,也是大家普遍都接受的,就是由APP和自己的Server发心跳,如果APP的网络不异常的话,它自己结束它的Server肯定是知道的。如果异常的话心跳断了,他也会判断出是结束了的。而且我们这边源站服务也会保证,你5秒钟没有数据就一定是结束的了,我们会把你的流给踢掉,这样就能达到用户的业务状态也是稳定的,我们的流媒体服务也是稳定的,而且耦合也会比较少。

这是我们实际遇到的一个坑,这个其实不难,只是看现在普遍云服务提供商还都是在用回掉的方式,所以我特别提一下另外还有一种可选的方式,效果更好。

播放的流程,播放器会先向他自己的服务请求播放地址,然后来我们这拉流,可以是鉴权也可以不鉴权,取决于它的业务形态。如果拉流失败,我们有一些定制化的操作,他用RTMP来拉流的话,我们会告诉他具体是什么错,包括鉴权失效,鉴权参数错误,还是这个流有问题,我们都会在状态告诉他的。这是之前用户提到的需求,说是播放需要知道哪里出了问题,所以我们尽量把状态码都特别详细的返回给用户。包括我们原站也有查询接口,如果他需要那种统一查询也可以来查。

1、推流端实现方案


2281533-d9f2546f0ff66dfa.jpg

推流端设计的原则总结下来就是自适应,推流谁都可以做,开源的也很多。但是为什么有的做得好,有的做得不好呢?就是看自适应做的好不好。

总结下来有三点自适应:
第一是帧率和码率自适应,这是大家都能想到的。我推流,如果网络卡了,我就降点帧率或者降一点码率,把这个事情做好,把流能正常推上去,不要卡顿。也是这张图里画到的,在发送网络的时候,我们做了一个QS模块,我们团队除了做工程化的人之外,还会有四五个博士专门做算法的。

在这里就有一些体现,我们在码率自适应的时候,是直接可以回馈给编码器的,让编码器动态调整自己的码率,尽量保证质量无损,传出来的视频码率下降,视频平滑。帧率的控制就比较简单了,当我们发现网络卡顿了,我们就会反馈给帧率控制模块。

在采集的时候做一些丢弃的操作,目的就是把我们发送的带宽降下来。这个我们是基于TCP做的,肯定没有UDP的效果好,UDP是我们下一步的尝试,现在还没有开始。因为UDP还涉及到源站的一些架构重构,我们还没有来得及做,现在基于TCP的效果其实已经不错了。后面除了这种简单的自适应之外,我们还加了一个算法类的,那个效果就会更明显。

第二种自适应是软硬自适应,这个很好理解,像硬件编码的优点就是手机不烫,缺点一大堆,用MediaRecorder的话,音视频很难同步,用MediaCodec的话,版本兼容有问题,现在还不太好普及。用软编的话码率低,画质好,除了CPU特别烫,别的都是优点。
热门机型有一些低端的,软编受不了的就改成硬编。因为硬编是体力工作,所以适配的机型肯定是有限的

第三个自适应,算法自适应。我们是真正的第一家能够把h.265做成商业化的公司。现在所有的都在提h.265,不知道大家对h.265了不了解,有没有人听说过h.265可以商业化在Web端无插件播放?我们现在做到了在赛扬机器上可以播30FPS的720P视频,在浏览器上不用装任何插件,这是我们持续优化的结果。当然这个不适合移动的场景,是我们在接另外一个场景的时候用到的。

在移动端我们做到了IOS手机720P编码,做到15FPS,然后CPU不会打满,可能是50%到70%之间。之前数据是打满一个核。这是因为我们之前有很多做算法的团队,最开始是做技术授权,后来想在一些产品上落地,移动直播其实是h.265的一个很好的落地的场景,为什么这么说呢?

推流端的任务是把更好的画质推上来,网络有限的情况下,我怎么能推上来更好的画质?h.265相对h.264来说能把带宽省掉30%。30%的概念是在视频点播类的应用里能省点钱,在初创应用来说根本就不在乎,因为主播更贵,谁在乎这样30%的带宽。

但是在移动推流就不一样了,30%是从480P到720P的变化,就是你本来只能推480P上来的画质,经过h.265这种编码之后能推上来720P的,主播的需求就是网络够好,CPU够好,我为什么不推更好的视频上去呢?这就是h.265的一个场景,我用算法的优势,你的机器只要能够让我做到用265来自适应,我就可以推上去更好的画质。

2、分发网络-多集群源站设计


2281533-edcff7c3f8a5e0ac.jpg

分发网络是躲在很远的一个地方了,我们当时设计的三个原则就是高并发、高可用、系统解耦,前两个很虚了,只要是做系统都会想怎么高并发,怎么高可用,怎么横向扩展最容易。

我们做了一个多源站,相对于很多公司在做单源站的方式,我们就是为了让用户能更好的触达我们的网络。在各个集群、各个城市做了多源站,这样怎么能做到横向的扩容和数据与业务中心的隔离,这种方案并不是很难,用一些存储做好同步其实也做到了。

一些开源服务,也做多分辨率适配,但是它所有的转码调度都是由它的流媒体服务来调起的。包括转码的生命周期也是流媒体服务来控制的,他们都在同级部署。其实这是有很大问题的,多分辨率适配和原画的推送和分发完全不是一个优先级的服务。做系统定级的时候就应该把它们分离开,应该分离在不同的系统来做。

在线转码是一个非常耗CPU的业务。一台现在很高端配置的24核机器,如果我想转一些画质比较好的视频,每个视频转三个分辨率,这样我转八路就把它打满了,这是很耗CPU的。如果我转了没人看,这个CPU就在那耗着,而且这个是不适合和源站混部的一个服务。

转码要和数据离的近,在那个源站集群的同一机房,我们会申请一些转码的资源,然后由核心机房来统一调度。我们把调度和具体的功能分离开,根据你这个流推到哪,我们就就近在哪里转码。转码也加了一些实时转码的策略。

为什么要做在线转码?因为推流端已经是尽最大努力把最好的画质、最高的带宽传上来。但是播放端不一定看得了,这样我们就需要把它转出来,而且h.265虽然好,但是有个最大的问题就是在移动端的浏览器上没有办法播。分享出来的必须是h.264,要不然去微信或者是QQ浏览器,你是看不了的。

我们做了两种策略,一种是有限的机器合理调度。我们的转码系统是个分布式,流水线式的,类似Storm那种系统,但是我们自己做得更适合转码。任务进来之后,我们第一个流程不是转,而是分析,看看你是要转成什么样,你是什么画质,大概会用什么CPU。

如果你的CPU占用很多,我会认为这是一个很难再次被调度的服务,比如你一下进来一个占四个核的转码服务,后来再来一堆占一个核的,肯定是一个核的比较好调度,这个机器资源紧张了,我可以给你调度另外一台机器,或者另外一台机器本来就有些空余,现在剩三个核,我接不了四个核的,我只能先接一个核的,所以我们会按优先级,优先分配高CPU占用的任务,然后才是低CPU占用的任务,在流式系统里,会在预分析之后把不同的任务扔进不同的优先级队列,这个优先级队列就承担着去转不同分辨率视频的职能。

而且在后头如果需要降级容灾的话,也是靠这个优先级队列来解决的,每个用户会有配额。我刚才说24和准24路,其实对于一个云服务公司来说这个量太小了。像我之前在百度做媒体云的时候,每天转码量是有30万,我觉得一个业务做大了,一天30万的转码量是很正常的。

这是考验并发的一个项目,怎么能做到尽量的把CPU打平,因为波峰波谷很明显。像h.265这个场景,我们是做了一套实时转码,有人分享就立刻给你转,让用户一旦开始分享的时候,能达到秒开的作用。但是你不看的时候,我们会有策略尽快帮你停下来。因为这种分享出去的视频并不是一个高并发的业务,有人看我们才给他转是个比较合理的场景。

对于那些低分辨率的现在也在逐步上灰度,不是说所有的你分发了,你发起了,我都给你转,我们会逐渐判断,有人看我们才转,尽量节省系统资源。后面也会考虑存储资源,因为每个机房都会有存储,存储是完全不用CPU的,它保证的是磁盘和IO,和我们完全是资源不复用的,是可以混部的,后面我们会考虑一步一步的混部。

CDN的分发环节,分发环节其实有很多东西是需要播放来配合的,比如说现在推流为了保证画质好,我会增加B帧,加大GOP,这样编码出来的视频质量会变好,代价就是我增加了GOP,那我的延迟就会大,用户一定是从上一个关键帧开始看,这样他看到的可能就是5秒甚至是10秒之前的视频,这个对社交类的移动直播是不可忍受的。既然有这种需求,源站就需要把之前的都保存好。但是怎么能让延时被消化掉,就要靠播放端。

3、播放器端实现方案


2281533-3cdcc9f520a2827d.jpg

这是播放端的实现框图,中间少画了一个地方。这就是个传统的播放器框图,没有体现出我们的核心的技术点,数据从网络接收进来之后,经过RTMP的Demux之后,我们是有一个模块的,这个模块会去判断当前视频是否需要被丢弃,这个原则也和我们接收缓存有关系,我们缓存配的是两秒,如果超过两秒,或者超过某一个其他的阈值的话,我们会开启丢弃的模式。

这个丢弃有多种策略,有的是直接丢掉帧,有的是快进。如果做过播放器就会知道,传统的视频追赶一般都是在视频解码之后来做追赶。解码就意味着会耗CPU,尤其是现在如果我想播720的视频,光是解码就基本上勉强实时的话,根本就没有什么追赶的余地了。

所以我们在算法上做了一些优化,我们拿到这个视频的时候会先去判断它是不是一个可以丢的,如果它是可以丢的,在解码之前我们就丢,但是这样丢会出问题,因为解码器会内部不连续,一旦解码器内部不连续了,它可能会产生黑屏,所以我们即使要丢,也是在解码器里边做了一些定制化的开发,还是把要丢的视频传进去,让它自己来丢,它不去解,这样就能达到更快速的把这个视频丢掉,赶上现在的实际主播的进度。

这样的话,如果我们网络状况很好,不担心以后抖动的话,我们能做到从推流到观看是2秒的延迟,但是一般我们都控制在4秒,就是为了防止抖动产生。

刚才说的是丢的这种逻辑,如果想快进,类似斗鱼那种,在一点进去之后,开始画面是很快过去的,但是没有音频,我们现在在做有音频的方式,视频在快进,音频也在快进,这样的话声音会变调,因为采样率变了。以前在做端的经验的时候,也做过这种变速不变调的算法,很多开源的,改改其实效果都能不错,这种算法只要做好逆向优化,放进来之后,音频也能保证不变调。

上一篇 下一篇

猜你喜欢

热点阅读