

# 使用 IVS iOS 广播 SDK 发布和订阅 \| 实时直播功能
<a name="ios-publish-subscribe"></a>

本文档将引导您完成使用 IVS 实时直播 iOS 广播 SDK 发布和订阅舞台所涉及的步骤。

## 概念
<a name="ios-publish-subscribe-concepts"></a>

三个核心概念构成了实时功能的基础：[舞台](#ios-publish-subscribe-concepts-stage)、[策略](#ios-publish-subscribe-concepts-strategy)和[渲染器](#ios-publish-subscribe-concepts-renderer)。设计目标是最大限度地减少构建有效产品所需的客户端逻辑量。

### 暂存区
<a name="ios-publish-subscribe-concepts-stage"></a>

`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
```

### Strategy
<a name="ios-publish-subscribe-concepts-strategy"></a>

`IVSStageStrategy` 协议为主机应用程序提供了一种方法，可以将所需的舞台状态传递给 SDK。需要实现三项函数：`shouldSubscribeToParticipant`、`shouldPublishParticipant` 和 `streamsToPublishForParticipant`。下面将进行详述。

#### 订阅参与者
<a name="ios-publish-subscribe-concepts-strategy-participants"></a>

```
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
    }
}
```

此操作用于创建舞台，在该舞台中，监管人可以监视所有来宾，而不会被来宾看见或听见。主机应用程序可以使用其他业务逻辑，让监管人看到彼此，但对来宾不可见。

#### 订阅参与者的配置
<a name="ios-publish-subscribe-concepts-strategy-participants-config"></a>

```
func stage(_ stage: IVSStage, subscribeConfigurationForParticipant participant: IVSParticipantInfo) -> IVSSubscribeConfiguration
```

如果正在订阅远程参与者（请参阅[订阅参与者](#ios-publish-subscribe-concepts-strategy-participants)），则 SDK 会询问主机应用程序有关该参与者的自定义订阅配置。此配置是可选的，允许主机应用程序控制订阅用户行为的某些方面。有关可配置内容的信息，请参阅 SDK 参考文档中的 [SubscribeConfiguration](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/interfaces/SubscribeConfiguration)。

以下是一个实现示例：

```
func stage(_ stage: IVSStage, subscribeConfigurationForParticipant participant: IVSParticipantInfo) -> IVSSubscribeConfiguration {
    let config = IVSSubscribeConfiguration()

    try! config.jitterBuffer.setMinDelay(.medium())

    return config
}
```

此实现将所有已订阅参与者的抖动缓冲区最小延迟更新为预设的 `MEDIUM`。

与 `shouldSubscribeToParticipant` 一样，可以实现更高级的实现。给定的 `ParticipantInfo` 可用于有选择地更新特定参与者的订阅配置。

建议使用默认行为。仅在需要更改特定行为时指定自定义配置。

#### 发布
<a name="ios-publish-subscribe-concepts-strategy-publishing"></a>

```
func stage(_ stage: IVSStage, shouldPublishParticipant participant: IVSParticipantInfo) -> Bool
```

连接到舞台后，SDK 会查询主机应用程序以查看特定参与者是否应该发布。仅对有权根据提供的令牌进行发布的本地参与者调用此操作。

以下是实施示例：

```
func stage(_ stage: IVSStage, shouldPublishParticipant participant: IVSParticipantInfo) -> Bool {
    return true
}
```

适用于用户总想发布的标准视频聊天应用程序。用户可以将音频和视频静音或取消静音，以便立即隐藏或被看见/听见。（他们也可以使用发布/取消发布，但这要慢得多。对于经常需要更改可见性的使用场景，静音/取消静音更可取。）

#### 选择要发布的流
<a name="ios-publish-subscribe-concepts-strategy-streams"></a>

```
func stage(_ stage: IVSStage, streamsToPublishForParticipant participant: IVSParticipantInfo) -> [IVSLocalStageStream]
```

这项操作用于在发布时确定应发布的音频和视频流。稍后将在 [Publish a Media Stream](#ios-publish-subscribe-publish-stream) 中对此进行更详细的介绍。

#### 更新策略
<a name="ios-publish-subscribe-concepts-strategy-updates"></a>

此策略是动态的：可以随时更改从上述任何函数返回的值。例如，如果主机应用程序希望最终用户点击按钮之前不要发布，则可以从 `shouldPublishParticipant`（类似于 `hasUserTappedPublishButton`）返回一个变量。当该变量根据最终用户的交互而发生变化时，调用 `stage.refreshStrategy()` 发送信号到 SDK，表明 SDK 应该查询策略以获取最新值，仅应用已更改的内容。如果 SDK 发现 `shouldPublishParticipant` 值已更改，它将启动发布流程。如果 SDK 查询和所有函数返回的值与之前相同，则 `refreshStrategy` 调用不会对阶段进行任何修改。

如果 `shouldSubscribeToParticipant` 的返回值从 `.audioVideo` 更改为 `.audioOnly`，则如果之前存在视频流，将删除所有返回值已更改的参与者的视频流。

通常，舞台使用该策略来最有效地应用以前和当前策略之间的差异，而主机应用程序无需担心正确管理该策略所需的所有状态。因此，可以将调用 `stage.refreshStrategy()` 视为一种只需少量计算的操作，因为除非策略发生变化，否则该调用什么都不会做。

### 渲染器
<a name="ios-publish-subscribe-concepts-renderer"></a>

`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`。

## 发布媒体流
<a name="ios-publish-subscribe-publish-stream"></a>

通过 `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]
}
```

## 显示并删除参与者
<a name="ios-publish-subscribe-participants"></a>

订阅完成后，您将通过渲染器的 `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`，因此在远程或本地退出操作期间，从用户界面中删除参与者无需自定义业务逻辑。

## 静音和取消静音媒体流
<a name="ios-publish-subscribe-mute-streams"></a>

`IVSLocalStageStream` 对象具有控制流是否静音的 `setMuted` 函数。可以在 `streamsToPublishForParticipant` 策略函数返回之前或之后在流上调用此函数。

**重要提示**：如果在调用 `refreshStrategy` 后 `streamsToPublishForParticipant` 返回了新的 `IVSLocalStageStream` 对象实例，将对舞台应用新流对象的静音状态。创建新 `IVSLocalStageStream` 实例时要小心，务必保持预期的静音状态。

## 监控远程参与者媒体静音状态
<a name="ios-publish-subscribe-mute-state"></a>

当参与者更改其视频或音频流的静音状态时，将使用一组已更改的流调用渲染器 `didChangeMutedStreams` 函数。使用 `IVSStageStream` 上的 `isMuted` 属性相应地更新您的用户界面：

```
func stage(_ stage: IVSStage, participant: IVSParticipantInfo, didChangeMutedStreams streams: [IVSStageStream]) {
    streams.forEach { stream in 
        /* stream.isMuted */
    }
}
```

## 创建舞台配置
<a name="ios-publish-subscribe-stage-config"></a>

要自定义舞台视频配置的值，请使用 `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 统计信息
<a name="ios-publish-subscribe-webrtc-stats"></a>

要获取发布流或订阅流的最新 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)")
      }
   }
}
```

## 获取参与者属性
<a name="ios-publish-subscribe-participant-attributes"></a>

如果您在 `CreateParticipantToken` 操作请求中指定属性，则可以在 `IVSParticipantInfo` 属性中看到这些属性：

```
func stage(_ stage: IVSStage, participantDidJoin participant: IVSParticipantInfo) {
    print("ID: \(participant.participantId)")
    for attribute in participant.attributes {
        print("attribute: \(attribute.key)=\(attribute.value)")
    }
}
```

## 嵌入消息
<a name="ios-publish-subscribe-embed-messages"></a>

IVSImageDevice 上的 `embedMessage` 方法允许您在发布期间将元数据有效载荷直接插入视频帧中。这为实时应用实现帧同步消息传递。消息嵌入仅在使用 SDK 进行实时发布（非低延迟发布）时可用。

嵌入式消息不能保证会到达订阅用户手中，因为它们直接嵌入在视频帧中并通过 UDP 传输，这并不能保证数据包的传输。传输过程中的数据包丢失可能会导致消息丢失，尤其是在网络条件不佳的情况下。为了缓解这种情况，`embedMessage` 方法包括一个 `repeatCount` 参数，用于在多个连续帧中复制消息，从而提高传输可靠性。此功能仅适用于视频流。

### 使用 embedMessage
<a name="ios-embed-messages-using-embedmessage"></a>

发布客户端可以使用 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)")
}
```

### 重复消息有效载荷
<a name="ios-embed-messages-repeat-payloads"></a>

`repeatCount` 用于跨多个帧复制消息以提高可靠性。该值必须在 0 到 30 之间。接收客户端必须有删除重复消息的逻辑。

```
try imageDevice.embedMessage(messageData, withRepeatCount: 5)

// repeatCount: 0-30, receiving clients should handle duplicates
```

### 读取嵌入式消息
<a name="ios-embed-messages-read-messages"></a>

有关如何读取来自传入流的嵌入式消息的信息，请参阅下面的“获取补充增强信息 (SEI)”。

## 获取补充增强信息（SEI）
<a name="ios-publish-subscribe-sei-attributes"></a>

补充增强信息（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
    		}
	}
}
```

## 在后台继续会话
<a name="ios-publish-subscribe-background-session"></a>

应用程序进入后台时，您可以继续在舞台上听到远程音频，但无法继续发送自己的图像和音频。您需要更新 `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()`。

## 联播分层编码
<a name="ios-publish-subscribe-layered-encoding-simulcast"></a>

“联播分层编码”是一项 IVS 实时直播功能，允许发布者发送多个不同质量的视频层，也允许订阅用户动态或手动配置这些层。[直播优化](real-time-streaming-optimization.md)部分会对该功能作详细介绍。

### 配置分层编码（发布者）
<a name="ios-layered-encoding-simulcast-configure-publisher"></a>

要以发布者身份启用“联播分层编码”，请在实例化时将以下配置添加到 `IVSLocalStageStream`：

```
// Enable Simulcast
let config = IVSLocalStageStreamVideoConfiguration()
config.simulcast.enabled = true

let cameraStream = IVSLocalStageStream(device: camera, configuration: config)

// Other Stage implementation code
```

根据您在视频配置中设置的分辨率，系统会按照“直播优化”**部分[默认层、质量和帧速率](real-time-streaming-optimization.md#real-time-streaming-optimization-default-layers)小节中的定义，对一定数量的层进行编码和发送。

此外，您还可以选择在联播配置中配置各个层：

```
// 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 参考文档。

### 配置分层编码（订阅用户）
<a name="ios-layered-encoding-simulcast-configure-subscriber"></a>

订阅用户无需执行任何操作来启用分层编码。如果发布者正发送联播层，则服务器默认会在各层之间动态调整，根据订阅用户的设备和网络状况选择最佳质量。

或者，要选择发布者正发送的显式层，有几个选项可用，如下所述。

### 选项 1：初始层质量偏好
<a name="ios-layered-encoding-simulcast-layer-quality-preference"></a>

使用 `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：首选直播层
<a name="ios-layered-encoding-simulcast-preferred-layer"></a>

`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 层帮助程序
<a name="ios-layered-encoding-simulcast-remotestagestream-helpers"></a>

`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 参考文档](https://aws.github.io/amazon-ivs-broadcast-docs/latest/ios/)中的 `IVSRemoteStageStream` 类。出于 `LayerSelected` 原因，如果返回 `UNAVAILABLE`，则表示无法选择请求的层。尽量在其所在位置选择，通常是质量较低的层，以保持流稳定性。

## 将舞台广播到 IVS 通道
<a name="ios-publish-subscribe-broadcast-stage"></a>

要广播舞台，请创建一个单独的 `IVSBroadcastSession`，然后按照上述用 SDK 进行广播的常规说明进行操作。`IVSStageStream` 上的 `device` 属性将是上面代码片段中所示的 `IVSImageDevice` 或 `IVSAudioDevice`；这些属性可以连接到 `IVSBroadcastSession.mixer`，从而以可自定义的布局广播整个舞台。

或者，您可以合成舞台并将其广播到 IVS 低延迟通道，以覆盖更多的观众。请参阅 IVS Low-Latency Streaming User Guide 中的 [Enabling Multiple Hosts on an Amazon IVS Stream](https://docs.aws.amazon.com//ivs/latest/LowLatencyUserGuide/multiple-hosts.html)。