Skip to content

PLStreamingKit

lawder edited this page Jan 18, 2017 · 6 revisions

#1 概述

PLStreamingKit 是一个适用于 iOS 的 RTMP 直播推流 SDK,可高度定制化和二次开发。特色是支持 H.264 硬编码,以及支持 AAC-LC 硬编码;同时,还根据移动网络环境的多变性,实现了一套可供开发者灵活选择的编码参数集合。

PLStreamingKit 不包括摄像头、麦克风等设备相关的资源获取,只包括音视频数据的编码处理和发送。对于需要 SDK 处理摄像头、麦克风采集的需求,推荐使用PLCameraStreamingKit

##1.1 功能以及版本

功能 描述 版本
支持硬件编码 更低的 CPU 占用及发热量
提供多码率可选 更自由的配置
提供 H.264 视频编码 多种 profile level 可设定
提供 AAC 音频编码 当前采用 AAC-LC
支持多分辨率编码 更可控的清晰度
提供 HeaderDoc 文档 开发中使用 Quick Help 即时阅读文档
支持 ARM7, ARM64 指令集 为最新设备优化
支持模拟器运行 不影响模拟器快速调试
支持 RTMP 协议直播推流 保证秒级实时性
提供音视频配置分离 配置解耦
支持推流时码率变更 更方便定制流畅度/清晰度策略
支持弱网丢帧策略 不必担心累计延时,保障实时性
支持 GPUImage 滤镜对接 轻松坐拥 125 种内置滤镜
支持后台推流 轻松实现边推流边聊天等操作

##1.2 特性

  • 硬件编码
  • 多码率可选
  • H.264 视频编码
  • AAC 音频编码
  • 多分辨率编码支持
  • HeaderDoc 文档支持
  • 内置生成安全的 RTMP 推流地址
  • ARM64 支持
  • 支持 RTMP 协议直播推流
  • 音视频配置分离
  • 推流时可变码率
  • 提供发送 buffer
  • 与 GPUImage 轻松对接

#2 阅读对象

本文档为技术文档,需要阅读者:

  • 具有基本的 iOS 开发能力
  • 准备接入七牛云直播

#3 开发准备

##3.1 设备以及系统

  • 设备要求:iPhone 5 及以上设备
  • 系统要求:iOS 8.0 以上

##3.2 前置条件

  • 已注册七牛账号
  • 通过 pili@qiniu.com 申请并已开通直播权限

##3.3 版本升级须知

v1.1.6 开始,在使用 SDK 之前,需要保证 PLStreamingEnv 被正确初始化,否则在初始化核心类 PLStreamingSession 的阶段会抛出异常,具体可参看Demo

[PLStreamingEnv initEnv];

#4 快速开始

##4.1 开发环境配置

  • Xcode 开发工具。App Store 下载地址
  • 安装 CocoaPods。了解 CocoaPods 使用方法。官方网站
  • 模拟器无法运行此程序,需要真机测试。

##4.2 导入 SDK

###4.2.1 使用 CocoaPods 导入

推荐使用 CocoaPods 的方式导入,步骤如下:

  • 在工作目录中创建名称为 Podfile 的文件
  • 在 Podfile 中添加如下一行:
pod 'PLMediaStreamingKit'
  • 在终端中运行
$ pod install

到此,你已完成了 PLStreamingKit 的依赖添加。

此外,如果你希望将 PLStreamingKit 从旧版本升级到新版本,可以在终端中运行

$ pod update

###4.2.2 手动导入

我们建议使用 CocoaPods 导入,如果由于特殊原因需要手动导入,可以按照如下步骤进行:

  • 将 Pod 目录下的文件加入到工程中;
  • https://github.com/qiniu/happy-dns-objc HappyDNS 目录下的所有文件加入到工程中;
  • https://github.com/pili-engineering/pili-librtmp Pod 目录下的所有文件加入到工程中;
  • 在工程对应 TARGET 中,右侧 Tab 选择 Build Phases,在 Link Binary With Libraries 中加入 UIKit、AVFoundation、CoreGraphics、CFNetwork、CoreMedia、AudioToolbox 这些 framework,并加入 libc++.tdb、libz.tdb 及 libresolv.tbd;
  • 在工程对应 TARGET 中,右侧 Tab 选择 Build Settings,在 Other Linker Flags 中加入 -ObjC 选项;

##4.3 初始化推流逻辑

###4.3.1 SDK 初始化 在 AppDelegate.m 中进行 SDK 初始化,如果未进行 SDK 初始化,在核心类 PLStreamingSession 初始化阶段将抛出异常

#import <PLMediaStreamingKit/PLStreamingEnv.h>

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [PLStreamingEnv initEnv];
    // Override point for customization after application launch.
    return YES;
}

###4.3.2 创建子类

创建 View Controller 如图所示:

选择 subclass 为 UIViewController,如图所示:

###4.3.3 添加引用

在 PLStreamViewController.m 中添加引用:

#import <PLMediaStreamingKit/PLStreamingKit.h>

###4.3.4 添加 session 属性

在 PLStreamViewController.m 添加 session 属性:

@property (nonatomic, strong) PLStreamingSession  *session;

###4.3.5 添加 App Transport Security Setting

如图所示:

##4.4 添加采集逻辑

###4.4.1 添加授权

在添加实际的采集代码之前,我们先添加获取授权的代码:

void (^noPermission)(void) = ^{
    NSString *log = @"No camera permission.";
    NSLog(@"%@", log);
};

void (^requestPermission)(void) = ^{
    [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
        if (granted) {
            permissionGranted();
        } else {
            noPermission();
        }
    }];
};

AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
switch (status) {
    case AVAuthorizationStatusAuthorized:
        permissionGranted();
        break;
    case AVAuthorizationStatusNotDetermined:
        requestPermission();
        break;
    case AVAuthorizationStatusDenied:
    case AVAuthorizationStatusRestricted:
    default:
        noPermission();
        break;
}

###4.4.2 音视频采集

  • 在 permissionGranted 我们加入实际的采集代码,其中有一些属性需要自行添加:
__weak typeof(self) wself = self;
void (^permissionGranted)(void) = ^{
    __strong typeof(wself) strongSelf = wself;

    NSArray *devices = [AVCaptureDevice devices];
    for (AVCaptureDevice *device in devices) {
        if ([device hasMediaType:AVMediaTypeVideo] && AVCaptureDevicePositionBack == device.position) {
            strongSelf.cameraCaptureDevice = device;
            break;
        }
    }

    if (!strongSelf.cameraCaptureDevice) {
        NSString *log = @"No back camera found.";
        NSLog(@"%@", log);
        [self logToTextView:log];
        return ;
    }

    AVCaptureSession *captureSession = [[AVCaptureSession alloc] init];
    AVCaptureDeviceInput *input = nil;
    AVCaptureVideoDataOutput *output = nil;

    input = [[AVCaptureDeviceInput alloc] initWithDevice:strongSelf.cameraCaptureDevice error:nil];
    output = [[AVCaptureVideoDataOutput alloc] init];

    strongSelf.cameraCaptureOutput = output;

    [captureSession beginConfiguration];
    captureSession.sessionPreset = AVCaptureSessionPreset640x480;

    // setup output
    output.videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)};

    dispatch_queue_t cameraQueue = dispatch_queue_create("com.pili.camera", 0);
    [output setSampleBufferDelegate:strongSelf queue:cameraQueue];

    // add input && output
    if ([captureSession canAddInput:input]) {
        [captureSession addInput:input];
    }

    if ([captureSession canAddOutput:output]) {
        [captureSession addOutput:output];
    }

    NSLog(@"%@", [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio]);

    // audio capture device
    AVCaptureDevice *microphone = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:microphone error:nil];
    AVCaptureAudioDataOutput *audioOutput = nil;

    if ([captureSession canAddInput:audioInput]) {
        [captureSession addInput:audioInput];
    }
    audioOutput = [[AVCaptureAudioDataOutput alloc] init];

    self.microphoneCaptureOutput = audioOutput;

    if ([captureSession canAddOutput:audioOutput]) {
        [captureSession addOutput:audioOutput];
    } else {
        NSLog(@"Couldn't add audio output");
    }

    dispatch_queue_t audioProcessingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
    [audioOutput setSampleBufferDelegate:self queue:audioProcessingQueue];

    [captureSession commitConfiguration];

    NSError *error;
    [strongSelf.cameraCaptureDevice lockForConfiguration:&error];
    strongSelf.cameraCaptureDevice.activeVideoMinFrameDuration = CMTimeMake(1.0, strongSelf.expectedSourceVideoFrameRate);
    strongSelf.cameraCaptureDevice.activeVideoMaxFrameDuration = CMTimeMake(1.0, strongSelf.expectedSourceVideoFrameRate);
    [strongSelf.cameraCaptureDevice unlockForConfiguration];

    strongSelf.cameraCaptureSession = captureSession;

    [strongSelf reorientCamera:AVCaptureVideoOrientationPortrait];

    AVCaptureVideoPreviewLayer* previewLayer;
    previewLayer =  [AVCaptureVideoPreviewLayer layerWithSession:captureSession];
    previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    __weak typeof(strongSelf) wself1 = strongSelf;
    dispatch_async(dispatch_get_main_queue(), ^{
        __strong typeof(wself1) strongSelf1 = wself1;
        previewLayer.frame = strongSelf1.view.layer.bounds;
        [strongSelf1.view.layer insertSublayer:previewLayer atIndex:0];
    });

    [strongSelf.cameraCaptureSession startRunning];
};

##4.5 创建流对象

###4.5.1 创建 streamJson

// streamJSON 是从服务端拿回的
//
// 从服务端拿回的 streamJSON 结构如下:
//    @{@"id": @"stream_id",
//      @"title": @"stream_title",
//      @"hub": @"hub_id",
//      @"publishKey": @"publish_key",
//      @"publishSecurity": @"dynamic", // or static
//      @"disabled": @(NO),
//      @"profiles": @[@"480p", @"720p"],    // or empty Array []
//      @"hosts": @{
//            ...
//      }
NSDictionary *streamJSON = @{ // 这里按照之前从服务端 SDK 中创建好的 stream json 结构填写进去 };
PLStream *stream = [PLStream streamWithJSON:streamJSON];

###4.5.2 创建视频和音频的配置对象

当前使用默认配置,之后可以深入研究按照自己的需求做更改。

PLVideoStreamingConfiguration *videoConfiguration = [PLVideoStreamingConfiguration defaultConfiguration];
PLAudioStreamingConfiguration *audioConfiguration = [PLAudioStreamingConfiguration defaultConfiguration];

###4.5.3 创建推流 session 对象

self.session = [[PLStreamingSession alloc] initWithVideoStreamingConfiguration:videoConfiguration
                                                   audioStreamingConfiguration:audioConfiguration
                                                                        stream:stream];

##4.6 预览摄像头拍摄效果

此时,运行现在的工程, 第一次运行时将会请求摄像头和麦克风的系统授权。同意授权后,摄像头拍摄的内容呈现在 previewView 。 至此完成了摄像头采集工作。

##4.7 添加推流操作

取一个最简单的场景,就是点击一个按钮,然后触发发起直播的操作。

###4.7.1 添加触发按钮

我们在 view 上添加一个按钮吧。 我们在 - (void)viewDidLoad 方法最后添加如下代码

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(0, 0, 100, 44);
button.center = CGPointMake(CGRectGetMidX([UIScreen mainScreen].bounds), CGRectGetHeight([UIScreen mainScreen].bounds) - 80);
[button addTarget:self action:@selector(actionButtonPressed:) forControlEvents:UIControlEventTouchUpInside];

###4.7.2 实现按钮动作

- (void)actionButtonPressed:(id)sender {
    [self.session startWithCompleted:^(BOOL success) {
        if (success) {
            NSLog(@"Streaming started.");
        } else {
            NSLog(@"Oops.");
        }
    }];
}

###4.7.3 完成首次推流操作

Done,没有额外的代码了,现在可以开始一次推流了。 如果运行后,点击按钮提示 Oops.,就要检查一下你之前创建 PLStream 对象时填写的 StreamJson 是否有漏填或者填错的内容。

##4.8 查看推流内容

###4.8.1 登录 pili.qiniu.com 查看内容

  • 登录 pili.qiniu.com
  • 登录 streamJson 中使用的 hub
  • 查看 stream 属性
  • 点击属性中的播放 URL 后的箭头,即可查看内容。

##4.9 DEMO下载

#5 功能使用

当你要深入理解 SDK 的一些参数及有定制化需求时,可以从高级功能部分中查询阅读,以下小节无前后依赖。

##5.1 音视频编码配置

PLStreamingKit 中通过不同的 configuration 设置不同的编码配置信息,对应的有:

  • PLVideoStreamingConfiguration 视频编码配置
  • PLAudioStreamingConfiguration 音频编码配置

配置生效的时刻有两个:

  • PLStreamingSession init 时传递对应的 configuration
  • 在推流前、推流中、推流结束后调用 -reloadVideoConfiguration:-reloadAudioConfiguration: 重置 configuration

需要注意的是,通过 reload 方法重置 configuration 时,需要确保传递的 configuration 与当前 streamingSession 已经持有的不是一个对象。

###5.1.1 视频编码参数

当不确定视频编码具体的参数该如何设定时,你可以选择 SDK 内置的几种视频编码质量。

1.Quality 的对比

Quality VideoSize FPS ProfileLevel Video BitRate(Kbps)
kPLVideoStreamingQualityLow1 272x480 24 Baseline AutoLevel 128
kPLVideoStreamingQualityLow2 272x480 24 Baseline AutoLevel 256
kPLVideoStreamingQualityLow3 272x480 24 Baseline AutoLevel 512
kPLVideoStreamingQualityMedium1 368x640 24 High AutoLevel 512
kPLVideoStreamingQualityMedium2 368x640 24 High AutoLevel 768
kPLVideoStreamingQualityMedium3 368x640 24 High AutoLevel 1024
kPLVideoStreamingQualityHigh1 720x1280 24 High AutoLevel 1024
kPLVideoStreamingQualityHigh2 720x1280 24 High AutoLevel 1280
kPLVideoStreamingQualityHigh3 720x1280 24 High AutoLevel 1536

2.自定义编码参数

当前的 PLVideoStreamingConfiguration 中可自行设定的参数有:

  • videoProfileLevel
    • H.264 编码时对应的 profile level 影响编码压缩算法的复杂度和编码耗能。设置的越高压缩率越高,算法复杂度越高,相应的可能带来发热量更大的情况
  • videoSize
    • 编码的分辨率,对于采集到的图像,编码前会按照这个分辨率来做拉伸裁剪
  • videoFrameRate
    • 即 FPS,每一秒所包含的视频帧数
  • videoMaxKeyframeInterval
    • 两个关键帧的帧间隔,一般设置为 FPS 的三倍
  • videoBitRate
    • 平均的编码码率,设定后编码时的码率并不会是恒定不变,静物较低,动态物体会相应升高。

PLStreamingKit 为了防止编码参数设定失败而导致编码失败,出现推流无视频的情况,依据 videoProfileLevel 限定了其他参数的范围,该限定范围针对 Quality 生成的配置同样有效。参见以下表格:

ProfileLevel Max VideoSize Max FPS Max Video BitRate(Mbps)
Baseline 30 (720, 480) 30 10
Baseline 31 (1280, 720) 30 14
Baseline 41 (1920, 1080) 30 50
Main 30 (720, 480) 30 10
Main 31 (1280, 720) 30 14
Main 32 (1280, 1024) 30 20
Main 41 (1920, 1080) 30 50
High 40 (1920, 1080) 30 25
High 41 (1920, 1080) 30 62.5

3.码率、fps、分辨对清晰度及流畅度的影响

对于码率(BitRate)、FPS(frame per second)、分辨率(VideoSize)三者的关系,有必要在这里做一些说明,以便你根据自己产品的需要可以有的放矢的调节各个参数。

一个视频流个人的感受一般来说会有卡顿、模糊等消极的情况,虽然我们都不愿意接受消极情况的出现,但是在 UGC 甚至 PGC 的直播场景中,都不可避免的要面对。因为直播推流实时性很强烈,所以为了保证这一实时性,在网络带宽不足或者上行速度不佳的情况下,都需要做出选择。

要么选择更好的流程度但牺牲清晰度(模糊),要么选择更好的清晰度但牺牲流畅度(卡顿),这一层的选择大多由产品决定。

一般来说,当选定了一个分辨率后,推流过程中就不会对分辨率做变更,但可以对码率和 FPS 做出调节,从而达到上述两种情况的选择。

效果 码率 FPS
流畅度 负相关 正相关
清晰度 正相关 负相关

通过这个关联,我们就可以容易的知道该如何从技术层面做出调整。在追求更好的流畅度时,我们可以适当降低码率,如果 FPS 已经较高(如 30)时,可以维持 FPS 不变更,如果此时因为码率太低而画面无法接受,可以再适当调低 FPS;在追求更清晰的画质时,可以提高码率,FPS 调节至 24 左右人眼大多还会识别为流畅,如果可以接受有轻微卡顿,那么可以将 FPS 设置的更低,比如 20 甚至 15。

总之,这三者之间一起构建其了画面清晰和视频流畅的感觉,但最终参数是否能满意需要自己不断调整和调优,从而满足产品层面的需求。

###5.1.2 音频编码参数

相比于视频繁杂的参数,当前 PLAudioStreamingConfiguration 提供的参数较为简单,当前音频编码最终输出为 AAC-LC。

Quality 的对比:

Quality Audio BitRate(Kbps)
kPLAudioStreamingQualityHigh1 64
kPLAudioStreamingQualityHigh2 96
kPLAudioStreamingQualityHigh3 128

###5.1.3 切换音视频配置

为了满足推流中因网络变更,网络拥塞等情况下对码率、FPS 等参数的调节,PLStreamingKit 提供了重置编码参数的方法,因为在重置编码器时会重新发送编码参数信息,可能触发播放器重置解码器或者清除缓存的操作(依据播放器自身行为而定),所以推流中切换编码参数时,观看短可能出现短暂(但视觉可感知)的卡顿。因此建议不要频繁的切换编码参数,进而避免因此带来的播放端体验问题。

  • 在推流前、推流中、推流结束后调用 -reloadVideoConfiguration:-reloadAudioConfiguration: 重置 configuration

需要注意的是,通过 reload 方法重置 configuration 时,需要确保传递的 configuration 与当前 streamingSession 已经持有的不是一个对象。

###5.1.4 建议编码参数

提示:以下为建议值,可根据产品需求自行更改调节。

UGC 场景,因为主播方所在的网络环境参差不齐,所以不易将码率设置的过高,此处我们给出建议设定

  • WiFi: video Medium1 或者自定义编码参数时设定码率为 400~500Kbps
  • 3G/4G: video Low2 或者自定义编码参数时设定码率为 200~300Kbps

PGC 场景,因为主播方所在网络一般都会有较高的要求,并且主播网络质量大多可以保障带宽充足,此处我们给出建议设定

  • WiFi: video High1 或者自定义编码参数时设定码率为 1000~1200Kbps
  • 3G/4G: video Medium2 或者自定义编码参数时设定码率为 600~800Kbps

对于 PGC 中的 3G/4G 场景,假定 PGC 时会配备较好的外置热点保证上行带宽充足。

###5.1.5 如何只推音频

当你只需要推送音频时,并不需要额外的增加代码,只需要在创建 PLStreamingSession 时,只传入 PLAudioStreamingConfiguration 对象即可,这样 PLStreamingSession 就不会在内部创建视频编码的相关内容,推流时也只会发音频配置信息和音频数据。

##5.2 DNS 优化

在大陆一些地区或特别的运营商线路,存在较为普遍的 DNS 劫持问题,而这对与依赖 DNS 解析 rtmp 流地址的 PLStreamingKit 来说是很糟糕的情况,为了解决这一问题,我们引入了 HappyDNS 这个库,以便可以实现 httpDNS,localDNS 等方式解决这类问题。

###5.2.1 HappyDNS

你可以点击这里跳转到 HappyDNS 的 GitHub 主页,在那里查看更详细的介绍和使用。

默认情况下,你所创建的 PLStreamingSession 对象,内部持有一个 HappyDNS 对应的 manager 对象,来负责处理 DNS 解析。

如果你期望按照不同的规则来做 DNS 解析,那么你可以在创建 PLStreamingSession 前,创建好自己的 QNDnsManager 对象,我们在 PLStreamingSession 中提供了一个 init 方法满足这类需求,你可以传递自己的 QNDnsManager 对象给 PLStreamingSession,从而定制化 DNS 解析。

##5.3 流状态获取

PLStreamingKit 中,通过反馈 PLStreamingSession 的状态来反馈流的状态。我们定义了几种状态,确保 PLStreamingSession 对象在有限的几个状态间切换,并可以较好的反应流的状态。

状态名 含义
PLStreamStateUnknow 初始化时指定的状态,不会有任何状态会跳转到这一状态
PLStreamStateConnecting RTMP 流链接中的状态
PLStreamStateConnected RTMP 已连接成功时的状态
PLStreamStateDisconnecting RTMP 正常断开时,正在断开的状态
PLStreamStateDisconnected RTMP 正常断开时,已断开的状态
PLStreamStateError 因非正常原因导致 RTMP 流断开,如包发送失败、流校验失败等

###5.3.1 state 状态回调

state 状态对应的 Delegate 回调方法是:

- (void)streamingSession:(PLStreamingSession *)session streamStateDidChange:(PLStreamState)state;

只有在正常连接,正常断开的情况下跳转的状态才会触发这一回调。所谓正常连接是指通过调用 -startWithCompleted: 方法使得流连接的各种状态,而所谓正常断开是指调用 -stop 方法使得流断开的各种状态。所以只有以下四种状态会触发这一回调方法。

  • PLStreamStateConnecting
  • PLStreamStateConnected
  • PLStreamStateDisconnecting
  • PLStreamStateDisconnected

###5.3.2 error 状态回调

error 状态对应的 Delegate 回调方法是

- (void)streamingSession:(PLStreamingSession *)session didDisconnectWithError:(NSError *)error;

除了调用 -stop 之外的所有导致流断开的情况,都被归属于非正常断开的情况,此时就会触发该回调。对于错误的处理,我们不建议触发了一次 error 后就停掉,最好可以在此时尝试有限次数的重连,详见重连小节。

###5.3.3 status 状态回调

除了 state 作为流本身状态的切换,我们还提供了流实时情况的反馈接口。

- (void)streamingSession:(PLStreamingSession *)session streamStatusDidUpdate:(PLStreamStatus *)status;

默认情况下,该回调每隔 3s 调用一次,每次包含了这 3s 内音视频的 fps 和总共的码率(注意单位是 kbps)。你可以通过 PLStreamingSession 的 statusUpdateInterval 属性来读取或更改这个回调的间隔。

###5.3.4 产品层面的反馈

status 的状态回调可以很好的反应发送情况,及网络是否流畅,是否拥塞。所以此处可以作为产品层面对弱网情况决策的一个入口。

一般的,当 status.videoFPS 比预设的 FPS 明显小时(小于等于 20%),并且维持几秒都是如此,那么就可以判定为当前主播所在的网络为弱网环境,可以给主播视觉上的提示,或者主动帮她降低编码配置,甚至直接断掉主播的流,这些都由具体的产品需求而定,而此处只是给出一个入口的提示和建议。

##5.4 视频滤镜渲染

PLStreamingKit 因为只负责编码推流,所以采集和音视频预处理的权利都在开发者自己手中,可以做出更灵活的决定,此处就以视频滤镜为例说明如何配合其他第三方滤镜库实现实时的滤镜处理。

###5.4.1 GPUImage 接入

GPUImage 作为当前 iOS 平台使用率最高的图像渲染引擎,可以轻松与 PLStreamingKit 对接,利用 GPUImage 已有的 125 个内置滤镜满足大部分的直播滤镜需求。

###5.4.2 滤镜实例

// 使用 GPUImageVideoCamera 获取摄像头数据
GPUImageVideoCamera *videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionBack];
videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait;

// 创建一个 filter
GPUImageSketchFilter *filter = [[GPUImageSketchFilter alloc] init];

CGRect bounds = [UIScreen mainScreen].bounds;
CGFloat width = CGRectGetWidth(bounds);
CGFloat height = width * 640.0 / 480.0;
GPUImageView *filteredVideoView = [[GPUImageView alloc] initWithFrame:(CGRect){0, 64, width, height}];

// Add the view somewhere so it's visible
[self.view addSubview:filteredVideoView];

[videoCamera addTarget:filter];
[filter addTarget:filteredVideoView];

// 创建一个 GPUImageRawDataOutput 作为 filter 的 Target
GPUImageRawDataOutput *rawDataOutput = [[GPUImageRawDataOutput alloc] initWithImageSize:CGSizeMake(480, 640) resultsInBGRAFormat:YES];
[filter addTarget:rawDataOutput];
__weak GPUImageRawDataOutput *weakOutput = rawDataOutput;
__weak typeof(self) wself = self;
[rawDataOutput setNewFrameAvailableBlock:^{
    __strong GPUImageRawDataOutput *strongOutput = weakOutput;
    __strong typeof(wself) strongSelf = wself;
    [strongOutput lockFramebufferForReading];

    //从 GPUImageRawDataOutput 中获取 CVPixelBufferRef
    GLubyte *outputBytes = [strongOutput rawBytesForImage];
    NSInteger bytesPerRow = [strongOutput bytesPerRowInOutput];
    CVPixelBufferRef pixelBuffer = NULL;
    CVPixelBufferCreateWithBytes(kCFAllocatorDefault, 480, 640, kCVPixelFormatType_32BGRA, outputBytes, bytesPerRow, nil, nil, nil, &pixelBuffer);
    [strongOutput unlockFramebufferAfterReading];
    if(pixelBuffer == NULL) {
        return ;
    }

    // 发送视频数据
    [strongSelf.session pushPixelBuffer:pixelBuffer completion:^{
        CVPixelBufferRelease(pixelBuffer);
    }];
}];

[videoCamera startCameraCapture];

视频采集及素描滤镜部分的代码就这么多,完整的可运行代码在 GitHub 的 Example 中找到并尝试运行。

##5.5 网络异常处理

直播中,网络异常的情况比我们能意料到的可能会多不少,常见的情况一般有:

  • 网络环境切换,比如 3G/4G 与 Wi-Fi 环境切换
  • 网络不可达,网络断开属于这一类
  • 带宽不足,可能触发发送失败
  • 上行链路不佳,直接影响流发送速度

作为开发者我们不能乐观的认为只要是 Wi-Fi 网就是好的,因为即便是 Wi-Fi 也有可能因为运营商上行限制,共享网络带宽等因素导致以上网络异常情况的出现。

为何在直播中要面对这么多的网络异常情况,而在其他上传/下载中很少遇到的,这是因为直播对实时性的要求使得它不得面对这一情况,即无论网络是否抖动,是否能一直良好,直播都要尽可能是可持续,可观看的状态。

对于网络环境的切换,通常需要 App 整体做出调整,不单单是针对直播,所以 PLStreamingKit 并未对这一情况做额外的监听,而是需要开发者自己对这些状态做出处理。

###5.5.1 重连

PLStreamingKit 自 v1.2.5 起内置了自动重连功能,但默认处于关闭状态。之所以默认关闭,一方面是考虑到 App 的业务逻辑场景多样而负责,对于直播重连的次数、时机、间隔都会有不同的需求,此时让开发者自己来决定是否重连,以及尝试重连的次数会更加合理;另一方面是兼容旧版本业务层面可能已实现的自动重连逻辑。

如果你想直接使用内置的自动重连功能,可通过将 PLStreamingSessionautoReconnectEnable 属性设置为 YES 来开启,并需要注意如下几点:

  • 自动重连次数上限目前设定为 3 次,重连的等待时间会由首次的 02s 之间逐步递增到第三次的 46s 之间
  • 等待重连期间,streamState 处于 PLStreamStateAutoReconnecting 状态,业务层可根据该状态来更新用户界面
  • 网络异常的 error Delegate 回调只有在达到最大重连次数后还未连接成功时才会被触发

若你想自己实现自动重连逻辑,可以利用以下网络异常所触发的 error Delegate 回调接口来添加相应的逻辑:

- (void)streamingSession:(PLStreamingSession *)session didDisconnectWithError:(NSError *)error;

你可以在这个方法内通过重新调用 -startWithCompleted: 方法来尝试重连。此处建议不要立即重连,而是采用重连间隔加倍的方式,比如共尝试 3 次重连,第一次等待 0.5s, 第二次等待 1s, 第三次等待 2s,这样的方式主要考虑到弱网时网络带宽的缓解需要时间,而加倍重连可以更容易在网络恢复的时候连接,而非在网络已经拥塞时还不断做无用功的重连。

当网络在 WWAN(3G/4G) 和 Wi-Fi 之间相互切换时,我们提供了一个回调 connectionChangeActionCallback 属性,它的函数签名如下

typedef BOOL(^ConnectionChangeActionCallback)(PLNetworkStateTransition transition);

该回调函数传入参数为当前网络的切换状态 PLNetworkStateTransition。 返回值为布尔值,YES表示在某种切换状态下允许推流自动重启,NO则代表该状态下不应自动重启。例如在 PLNetworkStateTransitionWWANToWiFi 状态,即网络从 3G/4G 切换到 Wi-Fi 后,基于节省流量等需求考虑,你可能需要进行一次快速的重连,使得数据可以通过 Wi-Fi 网络发送,此时返回 YES 即可。反之,如果推流过程中由 Wi-Fi 切换到 3G/4G,此时在未征得用户同意使用移动流量推流时,可返回 NO 不自动重启推流。以下为参考逻辑

session.connectionChangeActionCallback = ^(PLNetworkStateTransition transition) {
        switch (transition) {
            case PLNetworkStateTransitionWWANToWiFi:
                return YES;
                
            case PLNetworkStateTransitionWiFiToWWAN:
                return NO;
                
            default:
                break;
        }
        
        return NO;
    };

如果该属性未被初始化赋值,则 SDK 内部出于节省用户移动网络流量的目的,会默认在 Wi-Fi 切换到 3G/4G 时断开推流。此时,你可以自行监听网络状态,调用 -restartWithCompleted: 方法来快速重连。

5.5.2 弱网优化

移动直播过程中存在着各种各样的网络挑战。由于无线网络相对于有线网络,可靠性较低,会经常遇到信号覆盖不佳导致的高丢包、高延时等问题,特别是在用网高峰期,由于带宽有限,网络拥塞的情况时有发生。自 v2.1.3 起,PLCameraStreamingKit 内置了一套弱网优化方案,可以满足以下两个诉求:

  • 能动态地适应网络质量,即在质量不佳的网络下,能够自动下调视频编码的输出码率和帧率,而当网络质量恢复稳定时,输出码率和帧率也应得到相应回升,并能在调节过程中使得码率与帧率变化相对平稳。
  • 在直播端网络质量稳定时,确保编码器输出的码率和帧率恒定在一个期望的最高值,以提供良好的清晰度和流畅度。

这套弱网优化方案包含两个工作模块:

  • 自适应码率模块,能够在期望码率与设定的最低码率间做出调节,适应网络抖动引发的数据带宽变化。
  • 动态帧率模块,能够在期望帧率与一个最低帧率间做出调节,动态调整输出的视频数据量。

这两个模块可并行工作,可以单独开启或关闭,开发者可根据自己的业务场景来决定该方案的应用形态。一般情况下,如果开发者想使用该解决方案,建议将两个调节模块都开启,可达到我们测试的最佳效果。利用码率与帧率调整相互配合作用,一方面有效控制网络波动情况下的音视频数据发送量,缓解网络拥塞,同时又能给播放端带来流畅的观看体验;另一方面在网络质量恢复时能够确保音视频的码率帧率是以设定的最优质量配置进行推流,并且能使不同码率帧率配置切换时以一种更为平滑的方式进行,不会给观看端带来画质波动的突兀感。

自适应码率调节可以通过 PLStreamingSession 的如下接口开启:

- (void)enableAdaptiveBitrateControlWithMinVideoBitRate:(NSUInteger)minVideoBitRate;

其关闭接口为:

- (void)disableAdaptiveBitrateControl;

开启该机制时,需设置允许向下调节的最低码率(注意其单位为 bps,如设置最低为 200 Kbps,应传入参数值为 200*1024),以便使自动调整后的码率不会低于该范围。该机制根据网络吞吐量及 TCP 发送时间来调节推流的码率,在网络带宽变小导致发送缓冲区数据持续增长时,SDK 内部将适当降低推流码率,若情况得不到改善,则会重复该过程直至平均码率降至用户设置的最低值;反之,当一段时间内网络带宽充裕,SDK 将适当增加推流码率,直至达到预设的推流码率。

动态帧率的开关为 PLStreamingSessiondynamicFrameEnable 属性,开启后,自动调整的最大帧率不会超过预设在 videoStreamingConfiguration 配置中的 expectedSourceVideoFrameRate,最低不会小于 10 FPS。

默认情况下,这两个模块处于关闭状态,是为了兼容旧版本中开发者可能已自行实现的弱网调节机制。若开发者想使用我们的内置方案,请确保您自定义的机制已被关闭。

##5.6 录屏推流

PLStreamingKit 支持 iOS 10 新增的录屏推流 (ReplayKit Live) 功能,开发者可通过构建 App Extension 来调用推流 API 实现实时游戏直播等功能。需要注意的是,实时直播需要游戏或 App 本身实现对 ReplayKit 的支持。

###5.6.1 创建 Broadcast Upload Extension

在原有直播 App 中添加一个类型为 Broadcast Upload Extension 的新 Target,如图所示:

Xcode 会额外自动创建一个类型为 Broadcast UI Extension 的 Target,用于显示调用 Broadcast Upload Extension 的用户界面。

###5.6.2 添加推流管理类

创建推流 API 调用管理类,添加头文件引用:

#import <PLMediaStreamingKit/PLStreamingKit.h>

头文件参考

#import <Foundation/Foundation.h>

#import <PLMediaStreamingKit/PLStreamingKit.h>

@interface BroadcastManager : NSObject

@property (nonatomic, strong) PLStreamingSession *session;

+ (instancetype)sharedBroadcastManager;
- (PLStreamState)streamState;
- (void)pushVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)pushAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer withChannelID:(const NSString *)channelID;

@end

类实现参考

@interface BroadcastManager ()<PLStreamingSessionDelegate>

@end

@implementation BroadcastManager

static BroadcastManager *_instance;

- (instancetype)init
{
    if (self = [super init]) {
        [PLStreamingEnv initEnv];
        
        PLVideoStreamingConfiguration *videoConfiguration = [[PLVideoStreamingConfiguration alloc] initWithVideoSize:CGSizeMake(1280, 720) expectedSourceVideoFrameRate:24 videoMaxKeyframeInterval:24*3 averageVideoBitRate:1000*1024 videoProfileLevel:AVVideoProfileLevelH264High41];
        PLAudioStreamingConfiguration *audioConfiguration = [PLAudioStreamingConfiguration defaultConfiguration];
        audioConfiguration.inputAudioChannelDescriptions = @[kPLAudioChannelApp, kPLAudioChannelMic];
        
        self.session = [[PLStreamingSession alloc] initWithVideoStreamingConfiguration:videoConfiguration
                                                           audioStreamingConfiguration:audioConfiguration
                                                                                stream:nil];
        self.session.delegate = self;
        #warning 以下 pushURL 需替换为一个真实的流地址
        NSString *pushURL = nil;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self.session startWithPushURL:[NSURL URLWithString:pushURL] feedback:^(PLStreamStartStateFeedback feedback) {
                if (PLStreamStartStateSuccess == feedback) {
                    NSLog(@"connect success");
                } else {
                    NSLog(@"connect failed");
                }
            }];
        });
    }
    return self;
}

+ (void)initialize
{
    _instance = [[BroadcastManager alloc] init];
}

- (PLStreamState)streamState
{
    return self.session.streamState;
}

- (void)pushVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    [self.session pushVideoSampleBuffer:sampleBuffer];
}

- (void)pushAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer withChannelID:(const NSString *)channelID
{
    [self.session pushAudioSampleBuffer:sampleBuffer withChannelID:channelID completion:nil];
}

+ (instancetype)sharedBroadcastManager
{
    return _instance;
}

// 实现其他必要的协议方法

- (void)streamingSession:(PLStreamingSession *)session didDisconnectWithError:(NSError *)error
{
    NSLog(@"error : %@", error);
}

- (void)streamingSession:(PLStreamingSession *)session streamStatusDidUpdate:(PLStreamStatus *)status
{
    NSLog(@"%@", status);
}

@end

注意 PLAudioStreamingConfiguration 实例生成时必需注册音频流来源

audioConfiguration.inputAudioChannelDescriptions = @[kPLAudioChannelApp, kPLAudioChannelMic];

其中 kPLAudioChannelApp 对应于 RPSampleBufferTypeAudioApp,是 ReplayKit Live 回调的 app 音频数据,kPLAudioChannelMic 对应于 RPSampleBufferTypeAudioMic,是 ReplayKit Live 回调的 mic 音频数据。之所以需要显示声明,是为了在 PLStreamingKit 在音频编码前将两路音频流进行混音。

在自动生成的 SampleHandler.m 中实现 RPBroadcastSampleHandler 协议部分方法如下:

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
    if ([BroadcastManager sharedBroadcastManager].streamState == PLStreamStateConnected) {
        switch (sampleBufferType) {
            case RPSampleBufferTypeVideo:
                // Handle video sample buffer
                [[BroadcastManager sharedBroadcastManager] pushVideoSampleBuffer:sampleBuffer];
                break;
            case RPSampleBufferTypeAudioApp:
                // Handle audio sample buffer for app audio
                [[BroadcastManager sharedBroadcastManager] pushAudioSampleBuffer:sampleBuffer withChannelID:kPLAudioChannelApp];
                break;
            case RPSampleBufferTypeAudioMic:
                // Handle audio sample buffer for mic audio
                [[BroadcastManager sharedBroadcastManager] pushAudioSampleBuffer:sampleBuffer withChannelID:kPLAudioChannelMic];
                break;
                
            default:
                break;
        }
    }
}

###5.6.3 一些注意点

如果你使用 CocoaPods 管理依赖库,可能会在编译 broadcast extension target 时遇到 link error,此时请检查 Podfile 里是否为 broadcast extension target 添加相应的依赖;或者可检查以下工程设置是否更改:

#6 知识补充与建议

##6.1 丢帧策略

这一章节,我们谈谈丢帧策略,这关乎最终的直播观看体验,这里我们主要说明为何需要它,以及它带来的利弊。

###6.1.1 丢帧策略的必要性

我想可能没有人会喜欢在直播中出现丢帧,但是为何我们一定要实现并提供它呢?这是我们在最初提供出丢帧策略时也在反复考虑和一再讨论过的一个问题。

原因很简单,为了保证直播的实时性。

直播作为有别于录播的富媒体传播手段,它的第一要素就是实时,没有了实时,直播的价值就会荡然无存。保证实时性就需要确保录制端的数据尽可能少的累积,尽可能快的发送,但如果没有丢帧策略,在弱网环境下,就会因为待发送数据的不断堆积而产生累计延时,最终带来延时越来越大的情况。作为推流的发起端和推送端,推流 SDK 要考虑的还不单单是实时性这一点,因为移动设备的内存有限,而视频数据对内存的占用较大,所以在推流时还要确保不会因为待发送数据堆积过多而带来内存吃紧,触发 crash 等严重问题。所以我们需要也一定要在推流端提供丢帧策略。

丢帧的方式可以有很多种,其中有些较为粗暴,会触发各类问题,比如花屏,爆音,音画不同步等问题,在反复尝试和验证了各类的丢帧策略后,我们最终选定了优先保证音频传输且不触发花屏、爆音、音画不同步问题的技术方案。这一方案可以保证在带宽不足或上行速度不佳时,优先丢弃视频帧,保证音频的持续传输,在观看端至多出现画面跳帧的情况,但声音会是连续的片段,体验上不会认为是推流端断网,确保直播的继续进行。

###6.1.2 利弊

丢帧策略固然保证了直播的实时性和推流端相对的稳定性,但是它的弊端也是显然的,就是会带来观看端体验的不佳。

面对并接受这一弊端是必要,这样才可以做出更好的产品决策,我们建议从产品层面至少需要考虑两点

  • 在主播弱网情况下,观看端体验要保证流畅度优先还是清晰度优先
  • 在主播弱网情况下,尽可能让主播自己知晓自己网络不佳这一事实

对于流畅度和清晰度的问题,可以参考码率、fps、分辨对清晰度及流畅度的影响这一小节。

#7 API参考

#8 历史记录

  • 1.2.2 (Release Notes && API Diffs)
  • 功能
    • 支持初始化的时候传入 stream 为 nil
    • 支持调节编码采样率
    • 支持快速重连操作,方便 4G 推流时切换 WIFI 场景快速切换网络
    • 完善了音频出错时的 log
  • 1.2.1 (Release Notes && API Diffs)
    • 功能
      • 新增 iOS9 下的纯 IPV6 环境支持
    • 缺陷
      • 修复 dynamic 鉴权方式下重连失效的问题
  • 1.2.0 (Release Notes && API Diffs)
    • 解决 iPhone 6s 上出现的电流音问题
    • 支持后台推流
    • 支持 64kbps 音频码率
    • 部分接口重命名
  • 1.1.6 (Release Notes && API Diffs)
    • 拆分 pili-librtmp 为公共依赖,避免模拟器环境下与 PLPlayerKit冲突的问题
    • 解决网络不可达条件下 - (void)startWithCompleted:(void (^)(BOOL success))handler; 方法无回调的问题
    • 新增质量上报支持
    • 增加推流中实时变换采集音频参数的接口
  • 1.1.5 (Release Notes && API Diffs)
    • 修复 v1.1.1 版本引入的断网时引起的 UI 卡死问题,强烈建议 >= v1.1.1 的均做更新
  • 1.1.4 (Release Notes && API Diffs)
    • 新增网络连接接收超时接口和发送超时接口
  • 1.1.3 (Release Notes && API Diffs)
    • 优化网络发包,合并多个小包一起发送,提升带宽利用率
  • 1.1.2 (Release Notes && API Diffs)
    • 修复 dynamic 推流 nounce 取值过小,导致安卓端推流后,同一个流在 iOS 端推流会失败的问题
  • 1.1.1 (Release Notes && API Diffs)
    • 修复因 video configuration rtmp 发送时没读渠道发送 onMetaData 只有音频信息的问题
    • 添加版本信息读取方法
    • 添加实施推流状态的返回,便于开发者从推流端获取推流信息
  • 1.1.0 (Release Notes && API Diffs)
    • 重构 PLVideoStreamingConfiguration, 提供给开发者更大的视频编码定制自由度
    • PLVideoStreamingConfiguration 提供了 validate 方法, 确保 fast fail 减少开发者 app 携带不正确编码参数上线的可能性
    • 优化推送音视频数据, 添加了编码处理完后的回调
  • 1.0.3 (Release Notes && API Diffs)
    • 优化 dns 解析部分,补全 happydns 解析失败后的本地解析
  • 1.0.2 (Release Notes && API Diffs)
    • 修复 dns 解析失败时无 error 回调的问题
    • 优化音频数据默认为单声道,与 iOS 设备单声道采集贴近
    • 针对没有音频 configuration 的推流,优化发送的 onMetaData 信息,只携带视频信息,极大缩短 ffplay, ijkplayer 的等待时间
  • 1.0.1 (Release Notes && API Diffs)
    • 添加 HappyDNS, 优化 DNS 解析
    • 优化 TCP 发送层, 减少发包失败触发的错误
    • 修复推流时内存递增的问题
    • 修复切换 Quality 时,播放卡住的问题
  • 1.0.0 (Release Notes && API Diffs)
    • PLStreamingKit CocoaPods 版本发布
    • H.264 硬件编码
    • AAC 硬件编码
    • RTMP 推流支持
    • 弱网络环境音频优先的丢帧策略
    • 接口简明,便于和 Pili 直接对接使用,减少理解和开发成本

PLCameraStreamingKit

PLStreamingKit

Clone this wiki locally