最近两年,直播行业真是火遍了大江南北,各种直播平台层出不穷。恰好在微博实习的这段时间负责的就是直播推流这一块,之前对视频和音频处理都不太熟悉,所以正好借这个机会对视频、音频处理这块补习一下。

直播整个流程大体是这样的:

采集(视频/音频)-> 处理(美颜/滤镜)-> 编码 -> 封包 -> 推流 -> 转码 -> 拉流 -> 解码/渲染/播放

VideoCore是著名的RTMP推流库,其主要工作是从视频/音频采集一路到推流,很多第三方推流SDK都是基于VideoCore的。当然VideoCore自带特效处理是可以自定义的,譬如可以自己加入美颜逻辑等,VideoCore自身只含有简单的滤镜。

前奏:RTMP协议

Real Time Messaging Protocol(RTMP,实时消息传输协议) 由Adobe公司提出的基于TCP的流媒体传输协议,由于Flash对RTMP支持很好、适合长时间播放、延迟也相对较低,所以RTMP被广泛应用于直播市场。关于RTMP的具体介绍有兴趣的可以看这里。VideoCore采用的就是RTMP协议进行推流。

概览:整体逻辑

视频/音频采集
    |
视频(CVPixelBufferRef inputs & outputs -合并-> CVPixelBufferRef)
音频(raw LPCM buffers -合并-> LPCM buffer)
    |
添加滤镜(默认为空,但提供几个简单的滤镜)
    |
将buffer划分为多个outputs
    |
视频进行H.264编码、音频进行AAC编码
    |
将编码后的视频/音频数据进行封包
    |
建立RTMP连接并推送至服务端

编译时遇到的问题

  1. 使用CocoaPods 1.0导入VideoCore,编译时产生
error:type_half.inl not found

解决办法:Pods.xcodeproj的Build Settings——Header Search Paths,删掉${PODS_ROOT}/Headers/Private之后即可

  1. 解决问题1之后,出现以下问题
Undefined symbols for architecture arm64: "videocore::Apple::H264Encode::~H264Encode()",

这是由于VideoCore/transforms/Apple/目录下的H264Encode.h和H264Encode.mm没有添加进来,手动将其添加至Pods.xcodeproj的Development Pods/VideoCore/transforms/Apple/目录下即可

源码梳理

VideoCore大部分地方都写了注释,所以还是比较好理解的,这里不做过多详细的解释,只是对代码进行梳理,关键的地方会再次说明。

0. 目录结构

VideoCore
    | - /api/iOS
           |- ViewPreviewView 预览View
           |- VCSimpleSession 对各种行为的OC封装
    | - /filters 各种滤镜效果
    | - /mixers 混合器,raw LPCM buffers合并处理,添加滤镜等效果
    | - /rtmp 与服务器建立、断开连接及推送数据
    | - /sources 数据采集
            |- /iOS
                 |- CameraSource 视频采集
                 |- MicSource 音频采集
     | - stream 数据流(真正向服务器推送数据)
     | - system
     | - transforms 编码、封包相关
            | - /Apple
                 |- H264Encode 视频H.264编码(硬编)
            | - /iOS 编码
                  |- AACEncode 音频AAC编码
                  |- H264Encode 视频H.264编码(软编)
            | - /RTMP 封包

1. 门面: VCSimpleSession

这是VideoCore对外的接口,其实这个只是一层OC的封装,我们使用时消息都是发送给这个类的实例的。
Session的状态定义如下:

typedef NS_ENUM(NSInteger, VCSessionState)
{
    VCSessionStateNone,             // 未启动
    VCSessionStatePreviewStarted,   // 已启动预览
    VCSessionStateStarting,         // 正在连接服务器
    VCSessionStateStarted,          // 服务器已连接
    VCSessionStateEnded,            // 连接已结束
    VCSessionStateError             // 错误
};

这里也定义了几个常见的滤镜

typedef NS_ENUM(NSInteger, VCFilter) {
    VCFilterNormal,         // 正常
    VCFilterGray,           // 灰度
    VCFilterInvertColors,   // 反色
    VCFilterSepia,          // 暖调
    VCFilterFisheye,        // 鱼眼
    VCFilterGlow            //
};

属性列表如下:

@property (nonatomic, readonly) VCSessionState rtmpSessionState;         // 连接状态
@property (nonatomic, strong, readonly) UIView* previewView;             // 预览View
@property (nonatomic, assign) CGSize            videoSize;               // 视频分辨率,仅在初始化之前设置有效
@property (nonatomic, assign) int               bitrate;                 // 比特率,仅在初始化之前设置有效
@property (nonatomic, assign) int               fps;                     // 编码帧数,仅在初始化之前设置有效
@property (nonatomic, assign, readonly) BOOL    useInterfaceOrientation; //
@property (nonatomic, assign) VCCameraState cameraState;                 // 摄像头状态 前/后
@property (nonatomic, assign) BOOL          orientationLocked;           // 锁定方向
@property (nonatomic, assign) BOOL          torch;                       // 闪光灯
@property (nonatomic, assign) float         videoZoomFactor;             // 缩放比例
@property (nonatomic, assign) int           audioChannelCount;           // 声道
@property (nonatomic, assign) float         audioSampleRate;             // 音频采样率
@property (nonatomic, assign) float         micGain;                     // 麦克风的采集音量 [0..1],默认为1
@property (nonatomic, assign) CGPoint       focusPointOfInterest;        // 焦点 左上角为(0,0),右下角为(1,1)
@property (nonatomic, assign) CGPoint       exposurePointOfInterest;     // 测光中心点
@property (nonatomic, assign) BOOL          continuousAutofocus;         // 自动对焦
@property (nonatomic, assign) BOOL          continuousExposure;          // 动态测光
@property (nonatomic, assign) BOOL          useAdaptiveBitrate;          // 动态调整比特率
@property (nonatomic, readonly) int         estimatedThroughput;    /* Bytes Per Second. */
@property (nonatomic, assign) VCAspectMode  aspectMode;                  // 视频画面填充方式
@property (nonatomic, assign) VCFilter      filter;                      // 滤镜
@property (nonatomic, assign) id<VCSessionDelegate> delegate;

在init方法里,单独开了一个dispatch_queue来处理采集、处理、编码等操作防止阻塞主线程。

_graphManagementQueue = dispatch_queue_create("com.videocore.session.graph", 0);
__block VCSimpleSession* bSelf = self;
dispatch_async(_graphManagementQueue, ^{
    [bSelf setupGraph];
});

setupGraph方法里进行了采集、编码等处理。

- (void) startRtmpSessionWithURL:(NSString*) rtmpUrl
                    andStreamKey:(NSString*) streamKey;  // RTMP连接、推流

- (void) endRtmpSession;    // 断开RTMP连接

- (void) getCameraPreviewLayer: (AVCaptureVideoPreviewLayer**) previewLayer; //获取预览layer

- (void) addPixelBufferSource: (UIImage*) image withRect: (CGRect) rect; //向视频流中添加照片

2. 数据采集 CameraSource && MicSource

这两个类都继承自ISource,ISource主要定义了作为一个源的基本行为。注释写的都比较明白。
他们都还继承了另外一个类,比如CameraSource继承了std::enable_shared_from_this<CameraSource>,这其实是boost库的用法,继承该类就可以进行基于当前子类进行安全的weap_ptr到shared_ptr的转换。继承这个类,我们就可以直接在函数中调用shared_from_this获得该对象的shared_ptr。什么是shared_ptr呢?shared_ptr被称为智能指针,简单点说就是类似于Objective-C的引用计数,当引用计数变为0的时候,对象就会被销毁,这点我们应该是再熟悉不过了。

3. 视频处理(滤镜) FilterFactory

视频处理着这块切入点是在VCSimpleSession的filter属性的setter里,其调用的是GLESVideoMixer的setSourceFilter。GLESVideoMixer的基类定义了这样一个接口virtual void setSourceFilter(std::weak_ptr<ISource> source, IVideoFilter* filter)=0;,在这个接口里允许对视频源进行处理,GLESVideoMixer实现了这个方法。
在这里,使用了工厂模式来方便添加处理。当我们需要添加自定义处理比如说美颜的时候,也需要在以上几个切入点添加对应的逻辑,并最终使用FilterFactory产生我们需要的滤镜。

4. 将buffer划分为多个outputs Split

这样可以一方面输出预览,一方面进行编码打包及推流。假设我们新增一个需求要求将录像保存在本地的时候,则需要利用它再添加一个output来输出录像。

5. 编码 H264Encode && AACEncode

对于视频,若系统版本为iOS7,则利用AVAssetWriter编码,需要将视频写入本地文件,然后通过实时监听文件内容的改变,读取文件并进行编码封包。从iOS8开始,VideoToolBox.framework提供了硬件编码支持,可以使用VTCompressionSession进行硬件编码。
对于音频,则直接利用AudioToolbox.framework进行编码。

6. 封包 H264Packetizer && AACPacketizer

这里封包均封为RTMPMetadata_t格式的,方面之后推流使用。

7. 建立RTMP连接并推送至服务端 RTMPSession && StreamSession

RTMPSession 含有RTMP管理的操作(连接、推流及断开操作)。RTMPSession并不是真正流连接,而是对数据进行预处理以及链接操作的封装,其会利用StreamSession来创建一个m_streamSession,这个m_streamSession才真正承接传输具体的音视频数据和控制这些信息传输的命令信息的任务。逻辑上m_streamSession可以看作是RTMPSession的一个代理。
RMTP建立链接的过程如下图,m_streamSession接到来自Server的反馈后,它自身并不处理,而是反馈给RTMPSession,由RTMPSession来决定下一步发送什么。

每次向服务器推送的数据并不是直接发送RTMPMetadata_t,而是需要分割为一个个Chunk然后再进行发送。这个分割是在RTMPSession.mm中被实现的,数据被预处理完毕后存入m_streamInBuffer,之后m_streamSession再从buffer中读取数据并发送至服务器。

Preference

[1]带你吃透RTMP
[2]iOS RTMP上推直播视频