IVS iOS 广播 SDK 的高级用例 | 低延迟直播功能
我们将在此处介绍一些高级使用案例。从上面的基本设置开始,然后在此处继续。
创建广播配置
我们将在此处创建一个带有两个混合器插槽的自定义配置,允许我们将两个视频源绑定到混合器。一个 (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 }() ]
创建广播会话(高级版)
就像在基本示例中一样创建 IVSBroadcastSession
,但在此处会提供您的自定义配置。还对设备阵列提供 nil
,因为我们将手动添加它们。
let broadcastSession = try IVSBroadcastSession( configuration: config, // The configuration we created above descriptors: nil, // We’ll manually attach devices after delegate: self)
迭代和连接摄像机设备
我们将通过开发工具包检测到的输入设备进行迭代。开发工具包仅返回 iOS 中的内置设备。即使连接了蓝牙音频设备,它们也会显示为内置设备。有关更多信息,请参阅 IVS iOS 广播 SDK 中的已知问题和解决方法 | 低延迟直播功能。
找到我们想要使用的设备之后,我们就会调用 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 } }
交换摄像机
// 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)") } } }
创建自定义输入源
若要输入应用程序生成的声音或图像数据,请使用 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")
监控网络连接
移动设备通常会在使用过程中暂时失去并重新获得网络连接。因此,监控应用的网络连接并在事情发生变化时作出适当响应非常重要。
当广播连接断开时,广播开发工具包的状态将更改为 error
,然后变为 disconnected
。将通过 IVSBroadcastSessionDelegate
通知您发生了这些更改。当您收到这些状态更改时:
-
您的连接恢复之后,监控广播应用程序的连接状态,并使用端点和流密钥调用
start
。 -
重要提示:监控状态委托回调,并确保在再次调用
start
之后状态更改为connected
。
分离设备
如果要分离而不是替换设备,请使用 IVSDevice
或 IVSDeviceDescriptor
来分离它。
broadcastSession.detachDevice(currentCamera)
ReplayKit 集成
要在 iOS 上流式传输设备的屏幕和系统音频,必须与 ReplayKitIVSReplayKitBroadcastSession
,让 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 的广播扩展内存上限内。
获取推荐的广播设置
若要在开始广播之前评估用户的连接,请使用 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 { this.recommendation = result.recommendations[0]; } } }
使用自动重新连接
如果广播意外停止而未调用 stop
API(例如,网络连接暂时丢失),IVS 支持自动重新连接到广播。要启用自动重新连接,请将 IVSBroadcastConfiguration.autoReconnect
上的 enabled
属性设置为 true
。
当某些原因导致直播意外停止时,SDK 会按照线性退避策略重试最多 5 次。它通过 IVSBroadcastSessionDelegate.didChangeRetryState
函数将重试状态通知您的应用程序。
在后台,自动重新连接在提供的直播密钥末尾附加一个以 1 开头的优先级数字,以此来使用 IVS 直播接管功能。在 IVSBroadcastSession
实例的持续期间,每次尝试重新连接时,该数字都会增加 1。这意味着,如果设备的连接在广播期间丢失 4 次,并且每次丢失都需要重试 1-4 次,则上次直播的优先级可能介于 5 到 17 之间。因此,在 SDK 中为同一通道启用自动重新连接时,我们建议您不要使用其他设备的 IVS 直播接管。无法保证 SDK 当时使用的优先级,如果另一台设备接管,SDK 将尝试以更高的优先级进行重新连接。
使用后台视频
即使在后台播放应用程序,您也可以继续进行非 RelayKit 广播。
为了节省电力并保持前台应用程序的响应速度,iOS 一次只允许一个应用程序访问 GPU。Amazon IVS 广播开发工具包在视频管道的多个阶段使用 GPU,包括合成多个输入源、缩放图像和对图像进行编码。尽管广播应用程序在后台运行,但不能保证开发工具包能够执行任何这些操作。
使用 createAppBackgroundImageSource
方法解决此问题。它使开发工具包能够在后台继续广播视频和音频。它将返回 IVSBackgroundImageSource
,这是一个正常的 IVSCustomImageSource
,且具有额外的 finish
功能。每一个提供给背景图像源的 CMSampleBuffer
按照原始 IVSVideoConfiguration
提供的帧速率进行编码。CMSampleBuffer
上的时间戳将被忽略。
然后,开发工具包对这些图像进行缩放和编码并进行缓存,从而在应用程序进入后台时自动循环该源。当您的应用程序返回到前台时,连接的图像设备再次变为活动状态,并且预编码的流停止循环。
要撤消此过程,请使用 removeImageSourceOnAppBackgrounded
。除非您想显式恢复开发工具包的后台行为,否则您不必调用它;它将在取消分配 IVSBroadcastSession
时以其他方式被自动清除。
注意:我们强烈建议您在会话上线之前调用此方法,作为配置广播会话的一部分。该方法很昂贵(它对视频进行编码),因此在运行此方法时直播的性能可能会降低。
示例:为后台视频生成静态图像
向背景源提供单张图像会生成该静态图像的完整 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 的视频
您可以使用 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
可以更快地处理帧。
限制
您的应用程序需要后台音频授权
createAppBackgroundImageSource
只能在应用程序处于前台时被调用,因为它需要访问 GPU 才能完成。
createAppBackgroundImageSource
始终编码为完整的 GOP。例如,如果您的关键帧间隔为 2 秒(默认值)且运行速度为 30 fps,则会对 60 帧的倍数进行编码。
-
如果提供的帧少于 60 帧,则无论去除选项的值如何,都会重复最后一帧,直到达到 60 帧。
-
如果提供的帧数超过 60 帧并且去除选项为
true
,则最后 N 帧将被丢弃,其中 N 等于提交帧总数的剩余部分除以 60。 -
如果提供的帧数超过 60 帧并且去除选项为
false
,将重复最后一帧,直到达到 60 帧的下一个倍数。