使用 IVS iOS 广播 SDK 发布和订阅 | 实时直播功能
本文档将引导您完成使用 IVS 实时直播 iOS 广播 SDK 发布和订阅舞台所涉及的步骤。
概念
三个核心概念构成了实时功能的基础:舞台、策略和渲染器。设计目标是最大限度地减少构建有效产品所需的客户端逻辑量。
舞台
IVSStage 类是主机应用程序和 SDK 之间交互的主要点。此类表示舞台,用于加入和退出舞台。创建或加入舞台需要控制面板上有效的未过期令牌字符串(表示为 token)。加入和退出舞台很简单。
let stage = try IVSStage(token: token, strategy: self) try stage.join() stage.leave()
也可以将 IVSStageRenderer 和 IVSErrorDelegate 附加到 IVSStage 类:
let stage = try IVSStage(token: token, strategy: self) stage.errorDelegate = self stage.addRenderer(self) // multiple renderers can be added
策略
IVSStageStrategy 协议为主机应用程序提供了一种方法,可以将所需的舞台状态传递给 SDK。需要实现三项函数:shouldSubscribeToParticipant、shouldPublishParticipant 和 streamsToPublishForParticipant。下面将进行详述。
订阅参与者
func stage(_ stage: IVSStage, shouldSubscribeToParticipant participant: IVSParticipantInfo) -> IVSStageSubscribeType
当远程参与者加入舞台时,SDK 会向主机应用程序查询该参与者的所需订阅状态。选项为 .none、.audioOnly 和 .audioVideo。为该函数返回值时,主机应用程序无需担心发布状态、当前订阅状态或舞台连接状态。如果返回 .audioVideo,则 SDK 会等到远程参与者发布后再订阅,并在整个过程中通过渲染器更新主机应用程序。
以下是实施示例:
func stage(_ stage: IVSStage, shouldSubscribeToParticipant participant: IVSParticipantInfo) -> IVSStageSubscribeType { return .audioVideo }
完整实施此功能,适用于始终希望所有参与者都能看到对方的主机应用程序;例如,视频聊天应用程序。
也可以进行更高级的实施。根据服务器提供的属性,使用 IVSParticipantInfo 上的 attributes 属性有选择地订阅参与者:
func stage(_ stage: IVSStage, shouldSubscribeToParticipant participant: IVSParticipantInfo) -> IVSStageSubscribeType { switch participant.attributes["role"] { case "moderator": return .none case "guest": return .audioVideo default: return .none } }
此操作用于创建舞台,在该舞台中,监管人可以监视所有来宾,而不会被来宾看见或听见。主机应用程序可以使用其他业务逻辑,让监管人看到彼此,但对来宾不可见。
订阅参与者的配置
func stage(_ stage: IVSStage, subscribeConfigurationForParticipant participant: IVSParticipantInfo) -> IVSSubscribeConfiguration
如果正在订阅远程参与者(请参阅订阅参与者),则 SDK 会询问主机应用程序有关该参与者的自定义订阅配置。此配置是可选的,允许主机应用程序控制订阅用户行为的某些方面。有关可配置内容的信息,请参阅 SDK 参考文档中的 SubscribeConfiguration
以下是一个实现示例:
func stage(_ stage: IVSStage, subscribeConfigurationForParticipant participant: IVSParticipantInfo) -> IVSSubscribeConfiguration { let config = IVSSubscribeConfiguration() try! config.jitterBuffer.setMinDelay(.medium()) return config }
此实现将所有已订阅参与者的抖动缓冲区最小延迟更新为预设的 MEDIUM。
与 shouldSubscribeToParticipant 一样,可以实现更高级的实现。给定的 ParticipantInfo 可用于有选择地更新特定参与者的订阅配置。
建议使用默认行为。仅在需要更改特定行为时指定自定义配置。
发布
func stage(_ stage: IVSStage, shouldPublishParticipant participant: IVSParticipantInfo) -> Bool
连接到舞台后,SDK 会查询主机应用程序以查看特定参与者是否应该发布。仅对有权根据提供的令牌进行发布的本地参与者调用此操作。
以下是实施示例:
func stage(_ stage: IVSStage, shouldPublishParticipant participant: IVSParticipantInfo) -> Bool { return true }
适用于用户总想发布的标准视频聊天应用程序。用户可以将音频和视频静音或取消静音,以便立即隐藏或被看见/听见。(他们也可以使用发布/取消发布,但这要慢得多。对于经常需要更改可见性的使用场景,静音/取消静音更可取。)
选择要发布的流
func stage(_ stage: IVSStage, streamsToPublishForParticipant participant: IVSParticipantInfo) -> [IVSLocalStageStream]
这项操作用于在发布时确定应发布的音频和视频流。稍后将在 Publish a Media Stream 中对此进行更详细的介绍。
更新策略
此策略是动态的:可以随时更改从上述任何函数返回的值。例如,如果主机应用程序希望最终用户点击按钮之前不要发布,则可以从 shouldPublishParticipant(类似于 hasUserTappedPublishButton)返回一个变量。当该变量根据最终用户的交互而发生变化时,调用 stage.refreshStrategy() 发送信号到 SDK,表明 SDK 应该查询策略以获取最新值,仅应用已更改的内容。如果 SDK 发现 shouldPublishParticipant 值已更改,它将启动发布流程。如果 SDK 查询和所有函数返回的值与之前相同,则 refreshStrategy 调用不会对阶段进行任何修改。
如果 shouldSubscribeToParticipant 的返回值从 .audioVideo 更改为 .audioOnly,则如果之前存在视频流,将删除所有返回值已更改的参与者的视频流。
通常,舞台使用该策略来最有效地应用以前和当前策略之间的差异,而主机应用程序无需担心正确管理该策略所需的所有状态。因此,可以将调用 stage.refreshStrategy() 视为一种只需少量计算的操作,因为除非策略发生变化,否则该调用什么都不会做。
渲染器
IVSStageRenderer 协议将舞台状态传递给主机应用程序。渲染器提供的事件通常完全可以支持主机应用程序界面的更新。渲染器提供以下函数:
func stage(_ stage: IVSStage, participantDidJoin participant: IVSParticipantInfo) func stage(_ stage: IVSStage, participantDidLeave participant: IVSParticipantInfo) func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChange publishState: IVSParticipantPublishState) func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChange subscribeState: IVSParticipantSubscribeState) func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didAdd streams: [IVSStageStream]) func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didRemove streams: [IVSStageStream]) func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChangeMutedStreams streams: [IVSStageStream]) func stage(_ stage: IVSStage, didChange connectionState: IVSStageConnectionState, withError error: Error?) func stage(_ stage: IVSStage, participant: IVSParticipantInfo, stream: IVSRemoteStageStream, didChangeStreamAdaption adaption: Bool) func stage(_ stage: IVSStage, participant: IVSParticipantInfo, stream: IVSRemoteStageStream, didChange layers: [IVSRemoteStageStreamLayer]) func stage(_ stage: IVSStage, participant: IVSParticipantInfo, stream: IVSRemoteStageStream, didSelect layer: IVSRemoteStageStreamLayer?, reason: IVSRemoteStageStream.LayerSelectedReason)
预计渲染器提供的信息不会影响策略的返回值。例如,调用 participant:didChangePublishState 时,shouldSubscribeToParticipant 的返回值预计不会改变。如果主机应用程序想要订阅特定参与者,则无论该参与者的发布状态如何,它都应返回所需的订阅类型。SDK 负责确保根据舞台状态在正确的时间执行策略的期望状态。
请注意,只有发布参与者才会触发 participantDidJoin,每当参与者停止发布或退出舞台会话时,都会触发 participantDidLeave。
发布媒体流
通过 IVSDeviceDiscovery 发现内置麦克风和摄像头等本地设备。以下示例演示如何选择前置摄像头和默认麦克风,然后将它们作为 IVSLocalStageStreams 返回,由 SDK 发布:
let devices = IVSDeviceDiscovery().listLocalDevices() // Find the camera virtual device, choose the front source, and create a stream let camera = devices.compactMap({ $0 as? IVSCamera }).first! let frontSource = camera.listAvailableInputSources().first(where: { $0.position == .front })! camera.setPreferredInputSource(frontSource) let cameraStream = IVSLocalStageStream(device: camera) // Find the microphone virtual device and create a stream let microphone = devices.compactMap({ $0 as? IVSMicrophone }).first! let microphoneStream = IVSLocalStageStream(device: microphone) // Configure the audio manager to use the videoChat preset, which is optimized for bi-directional communication, including echo cancellation. IVSStageAudioManager.sharedInstance().setPreset(.videoChat) // This is a function on IVSStageStrategy func stage(_ stage: IVSStage, streamsToPublishForParticipant participant: IVSParticipantInfo) -> [IVSLocalStageStream] { return [cameraStream, microphoneStream] }
显示并删除参与者
订阅完成后,您将通过渲染器的 didAddStreams 函数接收一组 IVSStageStream 对象。要预览或接收有关该参与者的音频级别统计信息,您可以从流中访问底层 IVSDevice 对象:
if let imageDevice = stream.device as? IVSImageDevice { let preview = imageDevice.previewView() /* attach this UIView subclass to your view */ } else if let audioDevice = stream.device as? IVSAudioDevice { audioDevice.setStatsCallback( { stats in /* process stats.peak and stats.rms */ }) }
当参与者停止发布或取消订阅时,将使用已删除的流来调用 didRemoveStreams 函数。主机应用程序应将其用作信号,从视图层次结构中删除参与者的视频流。
在所有可能删除流的场景中都会调用 didRemoveStreams,包括:
-
远程参与者停止发布。
-
本地设备取消订阅或将订阅从
.audioVideo更改为.audioOnly。 -
远程参与者退出舞台。
-
本地参与者退出舞台。
由于在所有场景中都会调用 didRemoveStreams,因此在远程或本地退出操作期间,从用户界面中删除参与者无需自定义业务逻辑。
静音和取消静音媒体流
IVSLocalStageStream 对象具有控制流是否静音的 setMuted 函数。可以在 streamsToPublishForParticipant 策略函数返回之前或之后在流上调用此函数。
重要提示:如果在调用 refreshStrategy 后 streamsToPublishForParticipant 返回了新的 IVSLocalStageStream 对象实例,将对舞台应用新流对象的静音状态。创建新 IVSLocalStageStream 实例时要小心,务必保持预期的静音状态。
监控远程参与者媒体静音状态
当参与者更改其视频或音频流的静音状态时,将使用一组已更改的流调用渲染器 didChangeMutedStreams 函数。使用 IVSStageStream 上的 isMuted 属性相应地更新您的用户界面:
func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChangeMutedStreams streams: [IVSStageStream]) { streams.forEach { stream in /* stream.isMuted */ } }
创建舞台配置
要自定义舞台视频配置的值,请使用 IVSLocalStageStreamVideoConfiguration:
let config = IVSLocalStageStreamVideoConfiguration() try config.setMaxBitrate(900_000) try config.setMinBitrate(100_000) try config.setTargetFramerate(30) try config.setSize(CGSize(width: 360, height: 640)) config.degradationPreference = .balanced
获取 WebRTC 统计信息
要获取发布流或订阅流的最新 WebRTC 统计信息,请使用 IVSStageStream 上的 requestRTCStats。收集完成后,您将通过 IVSStageStreamDelegate(可在 IVSStageStream 上设置)收到统计信息。要持续收集 WebRTC 统计信息,请在 Timer 上调用此函数。
func stream(_ stream: IVSStageStream, didGenerateRTCStats stats: [String : [String : String]]) { for stat in stats { for member in stat.value { print("stat \(stat.key) has member \(member.key) with value \(member.value)") } } }
获取参与者属性
如果您在 CreateParticipantToken 操作请求中指定属性,则可以在 IVSParticipantInfo 属性中看到这些属性:
func stage(_ stage: IVSStage, participantDidJoin participant: IVSParticipantInfo) { print("ID: \(participant.participantId)") for attribute in participant.attributes { print("attribute: \(attribute.key)=\(attribute.value)") } }
嵌入消息
IVSImageDevice 上的 embedMessage 方法允许您在发布期间将元数据有效载荷直接插入视频帧中。这为实时应用实现帧同步消息传递。消息嵌入仅在使用 SDK 进行实时发布(非低延迟发布)时可用。
嵌入式消息不能保证会到达订阅用户手中,因为它们直接嵌入在视频帧中并通过 UDP 传输,这并不能保证数据包的传输。传输过程中的数据包丢失可能会导致消息丢失,尤其是在网络条件不佳的情况下。为了缓解这种情况,embedMessage 方法包括一个 repeatCount 参数,用于在多个连续帧中复制消息,从而提高传输可靠性。此功能仅适用于视频流。
使用 embedMessage
发布客户端可以使用 IVSImageDevice 上的 embedMessage 方法将消息有效载荷嵌入到其视频流中。有效载荷的大小必须大于 0 KB 且小于 1KB。每秒插入的嵌入式消息数量不得超过每秒 10KB。
let imageDevice: IVSImageDevice = imageStream.device as! IVSImageDevice let messageData = Data("hello world".utf8) do { try imageDevice.embedMessage(messageData, withRepeatCount: 0) } catch { print("Failed to embed message: \(error)") }
重复消息有效载荷
repeatCount 用于跨多个帧复制消息以提高可靠性。该值必须在 0 到 30 之间。接收客户端必须有删除重复消息的逻辑。
try imageDevice.embedMessage(messageData, withRepeatCount: 5) // repeatCount: 0-30, receiving clients should handle duplicates
读取嵌入式消息
有关如何读取来自传入流的嵌入式消息的信息,请参阅下面的“获取补充增强信息 (SEI)”。
获取补充增强信息(SEI)
补充增强信息(SEI)NAL 单元用于在视频旁存储帧对齐的元数据。订阅客户端通过检查从发布者的 IVSImageDevice 发出来的 IVSImageDeviceFrame 对象上的 embeddedMessages 属性,可以从发布 H.264 视频的发布者那里读取 SEI 有效载荷。为此,请获取发布者的 IVSImageDevice,然后通过提供给 setOnFrameCallback 的回调来观察每一帧,如下例所示:
// in an IVSStageRenderer’s stage:participant:didAddStreams: function, after acquiring the new IVSImageStream let imageDevice: IVSImageDevice? = imageStream.device as? IVSImageDevice imageDevice?.setOnFrameCallback { frame in for message in frame.embeddedMessages { if let seiMessage = message as? IVSUserDataUnregisteredSEIMessage { let seiMessageData = seiMessage.data let seiMessageUUID = seiMessage.UUID // interpret the message's data based on the UUID } } }
在后台继续会话
应用程序进入后台时,您可以继续在舞台上听到远程音频,但无法继续发送自己的图像和音频。您需要更新 IVSStrategy 实施以停止发布,然后订阅 .audioOnly(或者 .none,如果适用)。
func stage(_ stage: IVSStage, shouldPublishParticipant participant: IVSParticipantInfo) -> Bool { return false } func stage(_ stage: IVSStage, shouldSubscribeToParticipant participant: IVSParticipantInfo) -> IVSStageSubscribeType { return .audioOnly }
然后调用 stage.refreshStrategy()。
联播分层编码
“联播分层编码”是一项 IVS 实时直播功能,允许发布者发送多个不同质量的视频层,也允许订阅用户动态或手动配置这些层。直播优化部分会对该功能作详细介绍。
配置分层编码(发布者)
要以发布者身份启用“联播分层编码”,请在实例化时将以下配置添加到 IVSLocalStageStream:
// Enable Simulcast let config = IVSLocalStageStreamVideoConfiguration() config.simulcast.enabled = true let cameraStream = IVSLocalStageStream(device: camera, configuration: config) // Other Stage implementation code
根据您在视频配置中设置的分辨率,系统会按照“直播优化”部分默认层、质量和帧速率小节中的定义,对一定数量的层进行编码和发送。
此外,您还可以选择在联播配置中配置各个层:
// Enable Simulcast let config = IVSLocalStageStreamVideoConfiguration() config.simulcast.enabled = true let layers = [ IVSStagePresets.simulcastLocalLayer().default720(), IVSStagePresets.simulcastLocalLayer().default180() ] try config.simulcast.setLayers(layers) let cameraStream = IVSLocalStageStream(device: camera, configuration: config) // Other Stage implementation code
或者,您可以创建最多三层的自定义层配置。如果您提供空数组或不提供任何值,则使用上面描述的默认值。层通过以下必需的属性 setter 进行描述:
-
setSize: CGSize; -
setMaxBitrate: integer; -
setMinBitrate: integer; -
setTargetFramerate: float;
从预设开始,您可以覆盖单个属性,也可以创建全新的配置:
// Enable Simulcast let config = IVSLocalStageStreamVideoConfiguration() config.simulcast.enabled = true let customHiLayer = IVSStagePresets.simulcastLocalLayer().default720() try customHiLayer.setTargetFramerate(15) let layers = [ customHiLayer, IVSStagePresets.simulcastLocalLayer().default180() ] try config.simulcast.setLayers(layers) let cameraStream = IVSLocalStageStream(device: camera, configuration: config) // Other Stage implementation code
有关配置单个层时可能触发的最大值、限制和错误,请参阅 SDK 参考文档。
配置分层编码(订阅用户)
订阅用户无需执行任何操作来启用分层编码。如果发布者正发送联播层,则服务器默认会在各层之间动态调整,根据订阅用户的设备和网络状况选择最佳质量。
或者,要选择发布者正发送的显式层,有几个选项可用,如下所述。
选项 1:初始层质量偏好
使用 subscribeConfigurationForParticipant 策略可以选择作为订阅用户要接收的初始层:
func stage(_ stage: IVSStage, subscribeConfigurationForParticipant participant: IVSParticipantInfo) -> IVSSubscribeConfiguration { let config = IVSSubscribeConfiguration() config.simulcast.initialLayerPreference = .lowestQuality return config }
默认情况下,系统总是先向订阅用户发送质量最低的层,而后慢慢增加到质量最高的层。这可以优化最终用户的带宽消耗,提供最佳的视频播放时间,从而减少网络较弱的用户的初始视频冻结。
以下选项适用于 InitialLayerPreference:
-
lowestQuality:服务器首先会提供质量最低的视频层。这会优化带宽消耗以及媒体播放时间。质量定义为视频大小、比特率和帧速率的组合。例如,720p 视频的质量低于 1080p 视频的质量。 -
highestQuality:服务器首先会提供质量最高的视频层。这会优化质量,也可能会增加媒体播放时间。质量定义为视频大小、比特率和帧速率的组合。例如,1080p 视频的质量优于 720p 视频的质量。
注意:要使初始层首选项(initialLayerPreference 调用)生效,必须重新订阅,因为这些更新不适用于有效订阅。
选项 2:首选直播层
preferredLayerForStream 策略方法可使您在直播开始后选择图层。此策略方法接收参与者和直播信息,因此您可以根据各个参与者选择图层。SDK 在特定事件发生时调用此方法,例如流图层变化、参与者状态更改或主机应用程序刷新策略时。
此策略方法返回 IVSRemoteStageStreamLayer 以下对象之一:
-
图层对象,比如
IVSRemoteStageStream.layers返回的图层对象。 -
null,表示不应选择任何层,优先选择动态自适应。
例如,以下策略会始终让用户选择质量最低的可用视频层:
func stage(_ stage: IVSStage, participant: IVSParticipantInfo, preferredLayerFor stream: IVSRemoteStageStream) -> IVSRemoteStageStreamLayer? { return stream.lowestQualityLayer }
要重置层选择并返回动态自适应,则在策略中返回 null 或“未定义”。在本示例中,appState 是占位符变量,表示主机应用程序状态。
func stage(_ stage: IVSStage, participant: IVSParticipantInfo, preferredLayerFor stream: IVSRemoteStageStream) -> IVSRemoteStageStreamLayer? { If appState.isAutoMode { return nil } else { return appState.layerChoice } }
选项 3:RemoteSageStream 层帮助程序
IVSRemoteStageStream 有几种帮助程序,可用于做出有关层选择的决定并向最终用户显示相应的选择:
-
层事件:除了
IVSRemoteStageStreamDelegate之外,IVSStageRenderer还有传达层和联播自适应变更的事件:-
func stream(_ stream: IVSRemoteStageStream, didChangeAdaption adaption: Bool) -
func stream(_ stream: IVSRemoteStageStream, didChange layers: [IVSRemoteStageStreamLayer]) -
func stream(_ stream: IVSRemoteStageStream, didSelect layer: IVSRemoteStageStreamLayer?, reason: IVSRemoteStageStream.LayerSelectedReason)
-
-
层方法:
IVSRemoteStageStream有几种帮助程序方法,可用于获取有关流和正在呈现之层的信息。这些方法适用于preferredLayerForStream策略中提供的远程流,以及通过func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didAdd streams: [IVSStageStream])公开的远程流。-
stream.layers -
stream.selectedLayer -
stream.lowestQualityLayer -
stream.highestQualityLayer -
stream.layers(with: IVSRemoteStageStreamLayerConstraints)
-
有关详细信息,请参阅 SDK 参考文档IVSRemoteStageStream 类。出于 LayerSelected 原因,如果返回 UNAVAILABLE,则表示无法选择请求的层。尽量在其所在位置选择,通常是质量较低的层,以保持流稳定性。
将舞台广播到 IVS 通道
要广播舞台,请创建一个单独的 IVSBroadcastSession,然后按照上述用 SDK 进行广播的常规说明进行操作。IVSStageStream 上的 device 属性将是上面代码片段中所示的 IVSImageDevice 或 IVSAudioDevice;这些属性可以连接到 IVSBroadcastSession.mixer,从而以可自定义的布局广播整个舞台。
或者,您可以合成舞台并将其广播到 IVS 低延迟通道,以覆盖更多的观众。请参阅 IVS Low-Latency Streaming User Guide 中的 Enabling Multiple Hosts on an Amazon IVS Stream。