

# IVS iOS 广播 SDK 的高级用例 \$1 低延迟直播功能
<a name="broadcast-ios-use-cases"></a>

我们将在此处介绍一些高级使用案例。从上面的基本设置开始，然后在此处继续。

## 创建广播配置
<a name="broadcast-ios-create-configuration"></a>

我们将在此处创建一个带有两个混合器插槽的自定义配置，允许我们将两个视频源绑定到混合器。一个 (`custom`) 是全屏幕并布置在另一个 (`camera`) 后面，它更小且位于右下角。请注意，对于 `custom` 插槽，我们不设置位置、大小或宽高比模式。因为我们不设置这些参数，所以插槽使用视频设置的大小和位置。

```
let config = IVSBroadcastConfiguration()
try config.audio.setBitrate(128_000)
try config.video.setMaxBitrate(3_500_000)
try config.video.setMinBitrate(500_000)
try config.video.setInitialBitrate(1_500_000)
try config.video.setSize(CGSize(width: 1280, height: 720))
config.video.defaultAspectMode = .fit
config.mixer.slots = [
    try {
        let slot = IVSMixerSlotConfiguration()
        // Do not automatically bind to a source
        slot.preferredAudioInput = .unknown
        // Bind to user image if unbound
        slot.preferredVideoInput = .userImage
        try slot.setName("custom")
        return slot
    }(),
    try {
        let slot = IVSMixerSlotConfiguration()
        slot.zIndex = 1
        slot.aspect = .fill
        slot.size = CGSize(width: 300, height: 300)
        slot.position = CGPoint(x: config.video.size.width - 400, y: config.video.size.height - 400)
        try slot.setName("camera")
        return slot
    }()
]
```

## 创建广播会话（高级版）
<a name="broadcast-ios-create-session-advanced"></a>

就像在[基本示例](broadcast-ios-getting-started.md#broadcast-ios-create-session)中一样创建 `IVSBroadcastSession`，但在此处会提供您的自定义配置。还对设备阵列提供 `nil`，因为我们将手动添加它们。

```
let broadcastSession = try IVSBroadcastSession(
   configuration: config, // The configuration we created above
   descriptors: nil, // We’ll manually attach devices after
   delegate: self)
```

## 迭代和连接摄像机设备
<a name="broadcast-ios-attach-camera"></a>

我们将通过开发工具包检测到的输入设备进行迭代。开发工具包仅返回 iOS 中的内置设备。即使连接了蓝牙音频设备，它们也会显示为内置设备。有关更多信息，请参阅 [IVS iOS 广播 SDK 中的已知问题和解决方法 \$1 低延迟直播功能](broadcast-ios-issues.md)。

找到我们想要使用的设备之后，我们就会调用 `attachDevice` 来连接它。

```
let frontCamera = IVSBroadcastSession.listAvailableDevices()
    .filter { $0.type == .camera && $0.position == .front }
    .first
if let camera = frontCamera {
    broadcastSession.attach(camera, toSlotWithName: "camera") { device, error in
        // check error
    }
}
```

## 交换摄像机
<a name="broadcast-ios-swap-cameras"></a>

```
// This assumes you’ve kept a reference called `currentCamera` that points to the current camera.
let wants: IVSDevicePosition = (currentCamera.descriptor().position == .front) ? .back : .front
// Remove the current preview view since the device will be changing.
previewView.subviews.forEach { $0.removeFromSuperview() }
let foundCamera = IVSBroadcastSession
        .listAvailableDevices()
        .first { $0.type == .camera && $0.position == wants }
guard let newCamera = foundCamera else { return }
broadcastSession.exchangeOldDevice(currentCamera, withNewDevice: newCamera) { newDevice, _ in
    currentCamera = newDevice
    if let camera = newDevice as? IVSImageDevice {
        do {
            previewView.addSubview(try finalCamera.previewView())
        } catch {
            print("Error creating preview view \(error)")
        }
    }
}
```

## 创建自定义输入源
<a name="broadcast-ios-create-input-source"></a>

若要输入应用程序生成的声音或图像数据，请使用 `createImageSource` 或者 `createAudioSource`。这两种方法都会创建虚拟设备（`IVSCustomImageSource` 和 `IVSCustomAudioSource`），这些设备可以像任何其他设备一样绑定到混合器。

这两种方法返回的设备通过其 `onSampleBuffer` 函数接受 `CMSampleBuffer`：
+ 对于视频源，像素格式必须为 `kCVPixelFormatType_32BGRA`、`420YpCbCr8BiPlanarFullRange` 或者 `420YpCbCr8BiPlanarVideoRange`。
+ 对于音频源，缓冲区必须包含线性 PCM 数据。

您无法一起使用 `AVCaptureSession` 与摄像机输入以馈送自定义图像源，同时还使用广播开发工具包提供的摄像机设备。如果要同时使用多个摄像机，请使用 `AVCaptureMultiCamSession` 并提供两个自定义图像源。

自定义图像源主要应与静态内容（如图像）或视频内容一起使用：

```
let customImageSource = broadcastSession.createImageSource(withName: "video")
try broadcastSession.attach(customImageSource, toSlotWithName: "custom")
```

## 监控网络连接
<a name="broadcast-ios-network-connection"></a>

移动设备通常会在使用过程中暂时失去并重新获得网络连接。因此，监控应用的网络连接并在事情发生变化时作出适当响应非常重要。

当广播连接断开时，广播开发工具包的状态将更改为 `error`，然后变为 `disconnected`。将通过 `IVSBroadcastSessionDelegate` 通知您发生了这些更改。当您收到这些状态更改时：

1. 您的连接恢复之后，监控广播应用程序的连接状态，并使用端点和流密钥调用 `start`。

1. **重要提示：**监控状态委托回调，并确保在再次调用 `start` 之后状态更改为 `connected`。

## 分离设备
<a name="broadcast-ios-detach-device"></a>

如果要分离而不是替换设备，请使用 `IVSDevice` 或 `IVSDeviceDescriptor` 来分离它。

```
broadcastSession.detachDevice(currentCamera)
```

## ReplayKit 集成
<a name="broadcast-ios-replaykit"></a>

要在 iOS 上流式传输设备的屏幕和系统音频，必须与 [ReplayKit](https://developer.apple.com/documentation/replaykit?language=objc) 集成。Amazon IVS 广播 SDK 使用 `IVSReplayKitBroadcastSession`，让 ReplayKit 集成变得很简单。在您的 `RPBroadcastSampleHandler` 子类中，创建 `IVSReplayKitBroadcastSession` 的实例，然后：
+ 在 `broadcastStarted` 中开启会话
+ 在 `broadcastFinished` 中停止会话

会话对象将有三个用于屏幕图像、应用程序音频和麦克风音频的自定义源。将 `processSampleBuffer` 中提供的 `CMSampleBuffers` 传递到这些自定义源。

要处理设备方向，您需要从示例缓冲区中提取 Replaykit 特定的元数据。使用以下代码：

```
let imageSource = session.systemImageSource;
if let orientationAttachment = CMGetAttachment(sampleBuffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil) as? NSNumber,
    let orientation = CGImagePropertyOrientation(rawValue: orientationAttachment.uint32Value) {
    switch orientation {
    case .up, .upMirrored:
        imageSource.setHandsetRotation(0)
    case .down, .downMirrored:
        imageSource.setHandsetRotation(Float.pi)
    case .right, .rightMirrored:
        imageSource.setHandsetRotation(-(Float.pi / 2))
    case .left, .leftMirrored:
        imageSource.setHandsetRotation((Float.pi / 2))
    }
}
```

可以使用 `IVSBroadcastSession` 而非 `IVSReplayKitBroadcastSession` 来集成 ReplayKit。但是，RePlaykit 特定变体有几处修改，以减少内部内存占用空间，从而保持在 Apple 的广播扩展内存上限内。

## 获取推荐的广播设置
<a name="broadcast-ios-recommended-settings"></a>

若要在开始广播之前评估用户的连接，请使用 `IVSBroadcastSession.recommendedVideoSettings` 来运行一个简短的测试。在测试运行时，您将收到多个建议，从最推荐到最不推荐的顺序排列。在此版本的开发工具包中，无法重新配置当前 `IVSBroadcastSession`，因此您必须取消分配它，然后使用推荐的设置创建一个新的。您将继续收到 `IVSBroadcastSessionTestResults`，直到 `result.status` 为 `Success` 或者 `Error`。您可以使用 `result.progress` 检查进度。

Amazon IVS 支持 8.5 Mbps 的最大比特率（对于其 `type` 为 `STANDARD` 或 `ADVANCED` 的通道），所以此方法返回的 `maximumBitrate` 永远不会超过 8.5 Mbps。考虑到网络性能的小波动，建议此方法返回的 `initialBitrate` 略低于测试中测量的真实比特率。（通常不建议使用 100% 的可用带宽。）

```
func runBroadcastTest() {
    self.test = session.recommendedVideoSettings(with: IVS_RTMPS_URL, streamKey: IVS_STREAMKEY) { [weak self] result in
        if result.status == .success {
            self?.recommendation = result.recommendations[0];
        }
    }
}
```

## 使用自动重新连接
<a name="broadcast-ios-auto-reconnect"></a>

如果广播意外停止而未调用 `stop` API（例如，网络连接暂时丢失），IVS 支持自动重新连接到广播。要启用自动重新连接，请将 `IVSBroadcastConfiguration.autoReconnect` 上的 `enabled` 属性设置为 `true`。

当某些原因导致直播意外停止时，SDK 会按照线性退避策略重试最多 5 次。它通过 `IVSBroadcastSessionDelegate.didChangeRetryState` 函数将重试状态通知您的应用程序。

在后台，自动重新连接在提供的直播密钥末尾附加一个以 1 开头的优先级数字，以此来使用 IVS [直播接管](streaming-config.md#streaming-config-stream-takeover)功能。在 `IVSBroadcastSession` 实例的持续期间，每次尝试重新连接时，该数字都会增加 1。这意味着，如果设备的连接在广播期间丢失 4 次，并且每次丢失都需要重试 1-4 次，则上次直播的优先级可能介于 5 到 17 之间。因此，*在 SDK 中为同一通道启用自动重新连接时，我们建议您不要使用其他设备的 IVS 直播接管*。无法保证 SDK 当时使用的优先级，如果另一台设备接管，SDK 将尝试以更高的优先级进行重新连接。

## 使用后台视频
<a name="broadcast-ios-background-video"></a>

即使在后台播放应用程序，您也可以继续进行非 RelayKit 广播。

为了节省电力并保持前台应用程序的响应速度，iOS 一次只允许一个应用程序访问 GPU。Amazon IVS 广播 SDK 在视频管道的多个阶段使用 GPU，包括合成多个输入源、缩放图像和对图像进行编码。尽管广播应用程序在后台运行，但不能保证开发工具包能够执行任何这些操作。

使用 `createAppBackgroundImageSource` 方法解决此问题。它使开发工具包能够在后台继续广播视频和音频。它将返回 `IVSBackgroundImageSource`，这是一个正常的 `IVSCustomImageSource`，且具有额外的 `finish` 功能。每一个提供给背景图像源的 `CMSampleBuffer` 按照原始 `IVSVideoConfiguration` 提供的帧速率进行编码。`CMSampleBuffer` 上的时间戳将被忽略。

然后，开发工具包对这些图像进行缩放和编码并进行缓存，从而在应用程序进入后台时自动循环该源。当您的应用程序返回到前台时，连接的图像设备再次变为活动状态，并且预编码的流停止循环。

要撤消此过程，请使用 `removeImageSourceOnAppBackgrounded`。除非您想显式恢复开发工具包的后台行为，否则您不必调用它；它将在取消分配 `IVSBroadcastSession` 时以其他方式被自动清除。

**注意：***我们强烈建议您在会话上线之前调用此方法，作为配置广播会话的一部分。*该方法很昂贵（它对视频进行编码），因此在运行此方法时直播的性能可能会降低。

### 示例：为后台视频生成静态图像
<a name="background-video-example-static-image"></a>

向背景源提供单张图像会生成该静态图像的完整 GOP。

下面是使用 CIImage 的示例：

```
// Create the background image source
guard let source = session.createAppBackgroundImageSource(withAttemptTrim: true, onComplete: { error in
    print("Background Video Generation Done - Error: \(error.debugDescription)")
}) else {
    return
}

// Create a CIImage of the color red.
let ciImage = CIImage(color: .red)

// Convert the CIImage to a CVPixelBuffer
let attrs = [
    kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue,
    kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue,
    kCVPixelBufferMetalCompatibilityKey: kCFBooleanTrue,
] as CFDictionary

var pixelBuffer: CVPixelBuffer!
CVPixelBufferCreate(kCFAllocatorDefault,
                    videoConfig.width,
                    videoConfig.height,
                    kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
                    attrs,
                    &pixelBuffer)

let context = CIContext()
context.render(ciImage, to: pixelBuffer)

// Submit to CVPixelBuffer and finish the source
source.add(pixelBuffer)
source.finish()
```

或者，您可以使用捆绑图像，而不是创建纯色 CIImage。此处显示的唯一代码是如何将 UIImage 转换为 CIImage 以便与上一个示例一起使用：

```
// Load the pre-bundled image and get it’s CGImage
guard let cgImage = UIImage(named: "image")?.cgImage else {
    return
}

// Create a CIImage from the CGImage
let ciImage = CIImage(cgImage: cgImage)
```

### 示例：带 AVAssetImageGenerator 的视频
<a name="background-video-example-avassetimagegenerator"></a>

您可以使用 `AVAssetImageGenerator` 生成来自 `AVAsset` 的 `CMSampleBuffers`（尽管不是通过 HLS 流`AVAsset`）：

```
// Create the background image source
guard let source = session.createAppBackgroundImageSource(withAttemptTrim: true, onComplete: { error in
    print("Background Video Generation Done - Error: \(error.debugDescription)")
}) else {
    return
}

// Find the URL for the pre-bundled MP4 file
guard let url = Bundle.main.url(forResource: "sample-clip", withExtension: "mp4") else {
    return
}
// Create an image generator from an asset created from the URL.
let generator = AVAssetImageGenerator(asset: AVAsset(url: url))
// It is important to specify a very small time tolerance.
generator.requestedTimeToleranceAfter = .zero
generator.requestedTimeToleranceBefore = .zero

// At 30 fps, this will generate 4 seconds worth of samples.
let times: [NSValue] = (0...120).map { NSValue(time: CMTime(value: $0, timescale: CMTimeScale(config.video.targetFramerate))) }
var completed = 0

let context = CIContext(options: [.workingColorSpace: NSNull()])

// Create a pixel buffer pool to efficiently feed the source
let attrs = [
    kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
    kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue,
    kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue,
    kCVPixelBufferMetalCompatibilityKey: kCFBooleanTrue,
    kCVPixelBufferWidthKey: videoConfig.width,
    kCVPixelBufferHeightKey: videoConfig.height,
] as CFDictionary
var pool: CVPixelBufferPool!
CVPixelBufferPoolCreate(kCFAllocatorDefault, nil, attrs, &pool)

generator.generateCGImagesAsynchronously(forTimes: times) { requestTime, image, actualTime, result, error in
    if let image = image {
        // convert to CIImage then CVpixelBuffer
        let ciImage = CIImage(cgImage: image)
        var pixelBuffer: CVPixelBuffer!
        CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer)
        context.render(ciImage, to: pixelBuffer)
        source.add(pixelBuffer)
    }
    completed += 1
    if completed == times.count {
        // Mark the source finished when all images have been processed
        source.finish()
    }
}
```

可以使用 `AVPlayer` 和 `AVPlayerItemVideoOutput` 生成 `CVPixelBuffers`。但是，这需要使用 `CADisplayLink` 并且更接近实时地执行，而 `AVAssetImageGenerator` 可以更快地处理帧。

### 限制
<a name="background-video-limitations"></a>

您的应用程序需要[后台音频授权](https://developer.apple.com/documentation/xcode/configuring-background-execution-modes)以避免在进入后台后被暂停。

`createAppBackgroundImageSource` 只能在应用程序处于前台时被调用，因为它需要访问 GPU 才能完成。

`createAppBackgroundImageSource` 始终编码为完整的 GOP。例如，如果您的关键帧间隔为 2 秒（默认值）且运行速度为 30 fps，则会对 60 帧的倍数进行编码。
+ 如果提供的帧少于 60 帧，则无论去除选项的值如何，都会重复最后一帧，直到达到 60 帧。
+ 如果提供的帧数超过 60 帧并且去除选项为 `true`，则最后 N 帧将被丢弃，其中 N 等于提交帧总数的剩余部分除以 60。
+ 如果提供的帧数超过 60 帧并且去除选项为 `false`，将重复最后一帧，直到达到 60 帧的下一个倍数。