

# IVS 广播 SDK \$1 实时直播功能
<a name="broadcast"></a>

Amazon Interactive Video Services（IVS）实时流式传输广播 SDK 适用于使用 Amazon IVS 构建应用程序的开发人员。此开发工具包旨在利用 Amazon IVS 架构，并将实现 Amazon IVS 的持续改进和新功能。作为本机广播开发工具包，它旨在最大限度地减少对应用程序以及用户有权访问应用程序所在设备的性能影响。

请注意，广播 SDK 用于发送和接收视频；也就是说，您对主机和观众使用相同的 SDK。无需使用单独的玩家 SDK。

您的应用程序可以利用 Amazon IVS 广播开发工具包的主要功能：
+ **高质量的流式传输** - 广播开发工具包支持高质量的流式传输。从相机捕获视频并以高达 720p 的分辨率进行编码。
+ **自动比特率调整** - 智能手机用户是移动的，因此他们的网络条件会在整个广播过程中发生变化。Amazon IVS 广播开发工具包会自动调整视频比特率，以适应不断变化的网络条件。
+ **支持纵向和横向** - 无论您的用户如何持有其设备，图像都会显示为顶部朝上并正确缩放。广播 SDK 支持纵向和横向画布大小。当用户从配置的方向旋转设备时，它会自动管理宽高比。
+ **安全流式传输** - 使用 TLS 对用户的广播进行加密，因此他们可以保护其流的安全。
+ **外部音频设备** - Amazon IVS 广播开发工具包支持音频插孔、USB 和蓝牙 SCO 外接麦克风。

## 平台要求
<a name="broadcast-platform-requirements"></a>

### 本机平台
<a name="broadcast-native-platforms"></a>


| 平台 | 受支持的版本 | 
| --- | --- | 
| Android |  9.0\$1：请注意，客户可以使用版本 6.0\$1 进行构建，但不能使用实时直播功能。  | 
| iOS |  14\$1  | 

IVS 至少支持 4 个主要 iOS 版本和 6 个主要 Android 版本。我们当前版本的支持可能会超出这些最低限度。如果主要版本不再受支持，将至少提前 3 个月通过 SDK 发布说明通知客户。

### 桌面浏览器
<a name="browser-desktop"></a>


| 浏览器 | 支持的平台 | 受支持的版本 | 
| --- | --- | --- | 
| Chrome | Windows、macOS | 两个主要版本（当前版本和最新版本） | 
| Firefox | Windows、macOS | 两个主要版本（当前版本和最新版本） | 
| 边缘 | Windows 8.1\$1 | 两个主要版本（当前版本和最新版本） 不包括 Edge Legacy | 
| Safari | macOS | 两个主要版本（当前版本和最新版本） | 

### 移动浏览器（iOS 和 Android）
<a name="browser-mobile"></a>


| 浏览器 | 支持的平台 | 受支持的版本 | 
| --- | --- | --- | 
| Chrome | iOS、Android | 两个主要版本（当前版本和最新版本） | 
| Firefox | Android | 两个主要版本（当前版本和最新版本） | 
| Safari | iOS | 两个主要版本（当前版本和最新版本） | 

#### 已知限制条件
<a name="browser-mobile-limitations"></a>
+ 在所有移动 Web 浏览器上，由于性能限制会导致视频伪影和黑屏，我们建议同时发布/订阅的发布者不超过三个。如果需要更多发布者，请配置[仅限音频发布和订阅](web-publish-subscribe.md#web-publish-subscribe-concepts-strategy-updates)。
+ 出于性能考虑和可能发生的崩溃，我们建议不要合成舞台并将其广播到 Android 移动网络上的频道。如果需要广播功能，请集成 [IVS 实时流式 Android 广播 SDK](broadcast-android.md)。

## Webviews
<a name="broadcast-webviews"></a>

Web 广播 SDK 不支持 Webviews 或 Weblike 环境（电视、控制台等）。有关移动实施，请参阅适用于 [Android](broadcast-android.md) 和 [iOS](broadcast-ios.md) 的 Real-Time Streaming Broadcast SDK Guide。

## 所需设备访问
<a name="broadcast-device-access"></a>

广播开发工具包需要访问设备的摄像头和麦克风，包括设备内置的摄像头和麦克风以及通过蓝牙、USB 或音频插孔连接的摄像头和麦克风。

## 支持
<a name="broadcast-support"></a>

广播 SDK 在不断改进。请参阅 [Amazon IVS 发布说明](release-notes.md)了解可用版本和已修复问题。如果合适，请在联系支持部门之前更新您的广播开发工具包版本，看看这是否解决了您的问题。

### 版本控制
<a name="broadcast-support-versioning"></a>

Amazon IVS 广播开发工具包使用[语义化版本](https://semver.org/)。

在此讨论中，假设：
+ 最新版本是 4.1.3。
+ 先前主要版本的最新版本为 3.2.4。
+ 版本 1.x 最新版本是 1.5.6。

最新版本的次要版本已添加向后兼容的新功能。在本例中，版本 4.2.0 已添加新功能。

最新版本的补丁版本已添加向后兼容、次要错误修复。在这里，版本 4.1.4 已添加次要错误修复。

向后兼容、主要错误修复处理方式不同；将在以下几个版本中添加：
+ 最新版本补丁版本。在本例中是版本 4.1.4。
+ 先前次要版本的补丁版本。在本例中是版本 3.2.5。
+ 最新版本 1.x 版本的补丁版本。在本例中是版本 1.5.7。

主要错误修复由 Amazon IVS 产品团队定义。典型示例包括关键安全更新和客户所需的其他选定修复。

**注意：**在上面的例子中，发布的版本递增但不会跳过任何数字（例如，从 4.1.3 到 4.1.4）。实际上，一个或多个补丁编号可能保留在内部而不发布，因此发布版本可以从 4.1.3 增加到 4.1.6。

# IVS 广播 SDK：Web 指南 \$1 实时直播功能
<a name="broadcast-web"></a>

IVS 低延迟实时流式传输 Web 广播 SDK 为开发人员提供了在 Web 上构建交互式实时体验的工具。此 SDK 适用于使用 Amazon IVS 构建 Web 应用程序的开发人员。

Web 广播 SDK 让参与者能够发送和接收视频。SDK 支持以下操作：
+ 加入舞台
+ 向舞台中的其他参与者发布媒体
+ 舞台中其他参与者订阅媒体
+ 管理和监控发布到舞台的视频和音频
+ 获取每个对等连接的 WebRTC 统计信息
+ 所有操作均来自 IVS 低延迟流式传输 Web 广播 SDK

**Web 广播 SDK 的最新版本：**1.33.0（[发布说明](https://docs.aws.amazon.com/ivs/latest/RealTimeUserGuide/release-notes.html#mar12-26-broadcast-web-rt)） 

**参考文档：**有关 Amazon IVS Web 广播 SDK 中最重要方法的信息，请参阅 [https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference)。请确保选择最新版本的 SDK。

**示例代码**：以下示例可以帮助您快速开始使用 SDK：
+ [简单播放](https://codepen.io/amazon-ivs/pen/RNwVBRK)
+ [简单的发布和订阅](https://codepen.io/amazon-ivs/pen/ZEqgrpo)
+ [全面的 React 实时协作演示](https://github.com/aws-samples/amazon-ivs-real-time-collaboration-web-demo/tree/main)

**平台要求**：有关支持的平台列表，请参阅 [Amazon IVS 广播 SDK](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/broadcast.html)

**注意：**从浏览器发布对最终用户来说非常方便，因为无需安装额外软件。但是，基于浏览器的发布受限于浏览器环境的约束和多变性。如果需要优先考虑稳定性（例如用于事件流），我们通常建议从非浏览器源（如 OBS Studio 或其他专用编码器）进行发布，这些方式通常能直接访问系统资源，从而规避浏览器限制。有关非浏览器发布选项的更多信息，请参阅[流摄取](rt-stream-ingest.md)文档。

# IVS Web 广播 SDK 入门 \$1 实时直播功能
<a name="broadcast-web-getting-started"></a>

本文档将引导您完成 IVS 实时直播 Web 广播 SDK 入门所涉及的步骤。

## 导入
<a name="broadcast-web-getting-started-imports"></a>

实时构建基块与根广播模块位于不同的命名空间中。

### 使用脚本标签
<a name="broadcast-web-getting-started-imports-script"></a>

Web 广播 SDK 作为 JavaScript 库分发，可在 [https://web-broadcast.live-video.net/1.33.0/amazon-ivs-web-broadcast.js](https://web-broadcast.live-video.net/1.33.0/amazon-ivs-web-broadcast.js) 检索。

可以在全局对象 `IVSBroadcastClient` 上找到以下示例中定义的类和枚举：

```
const { Stage, SubscribeType } = IVSBroadcastClient;
```

### 使用 npm
<a name="broadcast-web-getting-started-imports-npm"></a>

安装 `npm` 程序包：

```
npm install amazon-ivs-web-broadcast
```

也可以从程序包模块中导入类、枚举和类型：

```
import { Stage, SubscribeType, LocalStageStream } from 'amazon-ivs-web-broadcast'
```

### 服务器端渲染支持
<a name="broadcast-web-getting-started-imports-server-side-rendering"></a>

Web 广播 SDK 暂存区库无法在服务器端上下文中加载，因为它在加载时会引用库正常运行所需的浏览器基元。要解决此问题，请动态加载库，如 [Web Broadcast Demo using Next and React](https://github.com/aws-samples/amazon-ivs-broadcast-web-demo/blob/main/hooks/useBroadcastSDK.js#L26-L31) 中所示。

## 请求权限
<a name="broadcast-web-request-permissions"></a>

您的应用程序必须请求权限才能访问用户的摄像头和麦克风，并且必须使用 HTTPS 发送请求。（这不是 Amazon IVS 特有的；需要访问摄像头和麦克风的任何网站都需要请求权限。）

以下示例函数显示了如何请求和获取音频和视频设备的权限：

```
async function handlePermissions() {
   let permissions = {
       audio: false,
       video: false,
   };
   try {
       const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
       for (const track of stream.getTracks()) {
           track.stop();
       }
       permissions = { video: true, audio: true };
   } catch (err) {
       permissions = { video: false, audio: false };
       console.error(err.message);
   }
   // If we still don't have permissions after requesting them display the error message
   if (!permissions.video) {
       console.error('Failed to get video permissions.');
   } else if (!permissions.audio) {
       console.error('Failed to get audio permissions.');
   }
}
```

有关更多信息，请参阅 [Permissions API](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API) 和 [MediaDevices.getUserMedia()](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)。

## 列出可用的设备
<a name="broadcast-web-request-list-devices"></a>

要查看哪些设备可供捕获，请查询浏览器的 [MediaDevices.enumerateDevices()](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices) 方法：

```
const devices = await navigator.mediaDevices.enumerateDevices();
window.videoDevices = devices.filter((d) => d.kind === 'videoinput');
window.audioDevices = devices.filter((d) => d.kind === 'audioinput');
```

## 从设备中检索 MediaStream
<a name="broadcast-web-retrieve-mediastream"></a>

获取可用设备列表后，您可以从任意数量的设备中检索媒体流。例如，您可以使用 `getUserMedia()` 方法从摄像头中检索媒体流。

如果您想指定从哪个设备捕获媒体流，可以在媒体限制的 `audio` 或 `video` 部分明确设置 `deviceId`。或者，您可以省略 `deviceId`，让用户从浏览器提示中选择他们的设备。

您还可以使用 `width` 和 `height` 限制来指定理想的摄像头分辨率。（请在[此处](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#properties_of_video_tracks)阅读有关这些限制的更多信息。） SDK 会自动应用与您的最大广播分辨率相对应的宽度和高度限制；但是，最好自己也应用这些限制，以确保将源添加到 SDK 后源宽高比不会改变。

对于实时直播功能，请确保将媒体分辨率限制为 720p。具体而言，`getUserMedia` 和 `getDisplayMedia` 的宽度和高度限制值相乘不得超过 921600（1280\$1720）。

```
const videoConfiguration = {
  maxWidth: 1280,
  maxHeight: 720,
  maxFramerate: 30,
}

window.cameraStream = await navigator.mediaDevices.getUserMedia({
   video: {
       deviceId: window.videoDevices[0].deviceId,
       width: {
           ideal: videoConfiguration.maxWidth,
       },
       height: {
           ideal:videoConfiguration.maxHeight,
       },
   },
});
window.microphoneStream = await navigator.mediaDevices.getUserMedia({
   audio: { deviceId: window.audioDevices[0].deviceId },
});
```

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

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

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

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

### 舞台
<a name="web-publish-subscribe-concepts-stage"></a>

`Stage` 类是主机应用程序和 SDK 之间交互的主要点。此类表示舞台，用于加入和退出舞台。创建和加入舞台需要控制面板上有效的未过期令牌字符串（表示为 `token`）。加入和退出舞台很简单：

```
const stage = new Stage(token, strategy)

try {
   await stage.join();
} catch (error) {
   // handle join exception
}

stage.leave();
```

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

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

要使用已定义的策略，请将该策略传递给 `Stage` 构造函数。以下是应用程序的完整示例，该应用程序使用策略将参与者的网络摄像头发布到舞台并订阅所有参与者。以下部分详细说明了每个必需策略函数的用途。

```
const devices = await navigator.mediaDevices.getUserMedia({ 
   audio: true,
   video: {
        width: { max: 1280 },
        height: { max: 720 },
    } 
});
const myAudioTrack = new LocalStageStream(devices.getAudioTracks()[0]);
const myVideoTrack = new LocalStageStream(devices.getVideoTracks()[0]);

// Define the stage strategy, implementing required functions
const strategy = {
   audioTrack: myAudioTrack,
   videoTrack: myVideoTrack,

   // optional
   updateTracks(newAudioTrack, newVideoTrack) {
      this.audioTrack = newAudioTrack;
      this.videoTrack = newVideoTrack;
   },

   // required
   stageStreamsToPublish() {
      return [this.audioTrack, this.videoTrack];
   },

   // required
   shouldPublishParticipant(participant) {
      return true;
   },

   // required
   shouldSubscribeToParticipant(participant) {
      return SubscribeType.AUDIO_VIDEO;
   }
};

// Initialize the stage and start publishing
const stage = new Stage(token, strategy);
await stage.join();


// To update later (e.g. in an onClick event handler)
strategy.updateTracks(myNewAudioTrack, myNewVideoTrack);
stage.refreshStrategy();
```

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

```
shouldSubscribeToParticipant(participant: StageParticipantInfo): SubscribeType
```

当远程参与者加入舞台，SDK 会向主机应用程序查询该参与者的所需订阅状态。选项为 `NONE`、`AUDIO_ONLY` 和 `AUDIO_VIDEO`。为该函数返回值时，主机应用程序无需担心发布状态、当前订阅状态或舞台连接状态。如果返回 `AUDIO_VIDEO`，则 SDK 会等到远程参与者发布后再订阅，并在整个过程中通过发射事件来更新主机应用程序。

以下是实施示例：

```
const strategy = {
   
   shouldSubscribeToParticipant: (participant) => {
      return SubscribeType.AUDIO_VIDEO;
   }

   // ... other strategy functions
}
```

完整实施此功能，适用于始终希望所有参与者都能看到对方的主机应用程序；例如，视频聊天应用程序。

也可以进行更高级的实施。例如，假设应用程序在使用 CreateParticipantToken 创建令牌时提供一个 `role` 属性。根据服务器提供的属性，该应用程序可使用 `StageParticipantInfo` 上的 `attributes` 属性有选择地订阅参与者：

```
const strategy = {
   
   shouldSubscribeToParticipant(participant) {
      switch (participant.attributes.role) {
         case 'moderator':
            return SubscribeType.NONE;
         case 'guest':
            return SubscribeType.AUDIO_VIDEO;
         default:
            return SubscribeType.NONE;
      }
   }
   // . . . other strategies properties
}
```

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

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

```
subscribeConfiguration(participant: StageParticipantInfo): SubscribeConfiguration
```

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

以下是一个实现示例：

```
const strategy = {
   
   subscribeConfiguration: (participant) => {
      return {
         jitterBuffer: {
            minDelay: JitterBufferMinDelay.MEDIUM
         }  
      }

   // ... other strategy functions
}
```

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

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

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

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

```
shouldPublishParticipant(participant: StageParticipantInfo): boolean
```

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

以下是实施示例：

```
const strategy = {
   
   shouldPublishParticipant: (participant) => {
      return true;
   }

   // . . . other strategies properties
}
```

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

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

```
stageStreamsToPublish(): LocalStageStream[];
```

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

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

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

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

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

### Events
<a name="web-publish-subscribe-concepts-events"></a>

`Stage` 实例是事件发射器。使用 `stage.on()`，将舞台状态传递给主机应用程序。事件完全可以支持主机应用程序界面的更新。事件如下所示：

```
stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_PUBLISH_STATE_CHANGED, (participant, state) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_SUBSCRIBE_STATE_CHANGED, (participant, state) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => {})
stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_REMOVED, (participant, streams) => {})
stage.on(StageEvents.STAGE_STREAM_ADAPTION_CHANGED, (participant, stream, isAdapting) => ())
stage.on(StageEvents.STAGE_STREAM_LAYERS_CHANGED, (participant, stream, layers) => ())
stage.on(StageEvents.STAGE_STREAM_LAYER_SELECTED, (participant, stream, layer, reason) => ())
stage.on(StageEvents.STAGE_STREAM_MUTE_CHANGED, (participant, stream) => {})
stage.on(StageEvents.STAGE_STREAM_SEI_MESSAGE_RECEIVED, (participant, stream) => {})
```

对于其中大多数事件，提供相应的 `ParticipantInfo`。

预计事件提供的信息不会影响策略的返回值。例如，调用 `STAGE_PARTICIPANT_PUBLISH_STATE_CHANGED` 时，`shouldSubscribeToParticipant` 的返回值预计不会改变。如果主机应用程序想要订阅特定参与者，则无论该参与者的发布状态如何，它都应返回所需的订阅类型。SDK 负责确保根据舞台状态在正确的时间执行策略的期望状态。

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

使用上面[从设备检索 MediaStream](broadcast-web-getting-started.md#broadcast-web-retrieve-mediastream)中概述的步骤来检索本地设备（如麦克风和摄像头）。在示例中，我们使用 `MediaStream` 创建用于 SDK 发布的 `LocalStageStream` 对象列表：

```
try {
    // Get stream using steps outlined in document above
    const stream = await getMediaStreamFromDevice();

    let streamsToPublish = stream.getTracks().map(track => {
        new LocalStageStream(track)
    });

    // Create stage with strategy, or update existing strategy
    const strategy = {
        stageStreamsToPublish: () => streamsToPublish
    }
}
```

## 发布屏幕共享
<a name="web-publish-subscribe-publish-screenshare"></a>

除了用户的网络摄像头外，应用程序通常还需要发布屏幕共享。发布屏幕共享需要为暂存区创建一个额外的令牌，专门用于发布屏幕共享的媒体。使用 `getDisplayMedia` 并将分辨率限制为最大 720p。之后的步骤类似于将相机发布到暂存区。

```
// Invoke the following lines to get the screenshare's tracks
const media = await navigator.mediaDevices.getDisplayMedia({
   video: {
      width: {
         max: 1280,
      },
      height: {
         max: 720,
      }
   }
});
const screenshare = { videoStream: new LocalStageStream(media.getVideoTracks()[0]) };
const screenshareStrategy = {
   stageStreamsToPublish: () => {
      return [screenshare.videoStream];
   },
   shouldPublishParticipant: (participant) => {
      return true;
   },
   shouldSubscribeToParticipant: (participant) => {
      return SubscribeType.AUDIO_VIDEO;
   }
}
const screenshareStage = new Stage(screenshareToken, screenshareStrategy);
await screenshareStage.join();
```

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

订阅完成后，您将通过 `STAGE_PARTICIPANT_STREAMS_ADDED` 事件接收一组 `StageStream` 对象。该活动还为您提供参与者信息，以在显示媒体流时提供帮助：

```
stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => {
    let streamsToDisplay = streams;

    if (participant.isLocal) {
        // Ensure to exclude local audio streams, otherwise echo will occur
        streamsToDisplay = streams.filter(stream => stream.streamType === StreamType.VIDEO)
    }

    // Create or find video element already available in your application
    const videoEl = getParticipantVideoElement(participant.id);

    // Attach the participants streams
    videoEl.srcObject = new MediaStream();
    streamsToDisplay.forEach(stream => videoEl.srcObject.addTrack(stream.mediaStreamTrack));
})
```

当参与者停止发布或取消订阅流时，将使用已删除的流来调用 `STAGE_PARTICIPANT_STREAMS_REMOVED` 函数。主机应用程序应将其用作信号，从 DOM 中删除参与者的视频流。

在所有可能删除流的场景中都会调用 `STAGE_PARTICIPANT_STREAMS_REMOVED`，包括：
+ 远程参与者停止发布。
+ 本地设备取消订阅或将订阅从 `AUDIO_VIDEO` 更改为 `AUDIO_ONLY`。
+ 远程参与者退出舞台。
+ 本地参与者退出舞台。

由于在所有场景中都会调用 `STAGE_PARTICIPANT_STREAMS_REMOVED`，因此在远程或本地退出操作期间，从用户界面中删除参与者无需自定义业务逻辑。

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

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

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

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

当参与者更改其视频或音频的静音状态时，已更改的流列表会触发 `STAGE_STREAM_MUTE_CHANGED` 事件。使用 `StageStream` 上的 `isMuted` 属性相应地更新您的用户界面：

```
stage.on(StageEvents.STAGE_STREAM_MUTE_CHANGED, (participant, stream) => {
   if (stream.streamType === 'video' && stream.isMuted) {
       // handle UI changes for video track getting muted
   }
})
```

此外，您可以查看 [StageParticipantInfo](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference#stageparticipantinfo)，了解有关处于静音状态的是音频还是视频是静音的状态信息：

```
stage.on(StageEvents.STAGE_STREAM_MUTE_CHANGED, (participant, stream) => {
   if (participant.videoStopped || participant.audioMuted) {
       // handle UI changes for either video or audio
   }
})
```

## 获取 WebRTC 统计信息
<a name="web-publish-subscribe-webrtc-stats"></a>

使用 `requestQualityStats()` 方法可以访问本地和远程流的详细 WebRTC 统计数据。该方法适用于 LocalStageStream 和 RemoteSageStream 对象。它将返回全面的质量指标，包括网络质量、数据包统计数据、比特率信息和帧相关指标。

这是一种异步方法，您可以使用该方法通过 await 或链接承诺来检索统计信息。当统计数据不可用时，则会返回 `undefined`；例如，流未处于活动状态或内部统计数据不可用。如果统计数据可用，该方法将返回 [LocalVideoStats](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/interfaces/LocalVideoStats)、[LocalAudioStats](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/interfaces/LocalAudioStats)、[RemoteVideoStats](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/interfaces/RemoteVideoStats) 或 [RemoteAudioStats](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/interfaces/RemoteAudioStats) 对象，具体取决于流类型（远程、本地、视频还是音频）。

请注意，对于联播视频流，该阵列会包含多个统计对象（每层一个）。

**最佳实践**
+ 轮询频率：以合理的间隔时间（1-5 秒）调用 `requestQualityStats()` 以免影响性能
+ 错误处理：始终在处理之前检查返回的值是否为 `undefined`
+ 内存管理：不再需要流时清除间隔时间/超时
+ 网络质量：针对网络导致的性能下降问题，使用 `networkQuality` 收集用户反馈。有关详细信息，请参阅[网络质量](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/enumerations/NetworkQuality)。

**示例用法**

```
// For local streams
const localStats = await localVideoStream.requestQualityStats();
const audioStats = await localAudioStream.requestQualityStats();

// For remote streams
const remoteVideoStats = await remoteVideoStream.requestQualityStats();
const remoteAudioStats = await remoteAudioStream.requestQualityStats();

// Example: Monitor stats every 10 seconds
const statsInterval = setInterval(async () => {
   const stats = await localVideoStream.requestQualityStats();
   if (stats) {
      // Note: If simulcast is enabled, you may receive multiple 
      // stats records for each layer
      stats.forEach(layer => {
         const rid = layer.rid || 'default';
         console.log(`Layer ${rid}:`, {
            active: layer.active,
            networkQuality: layer.networkQuality,
            packetsSent: layer.packetsSent,
            bytesSent: layer.bytesSent,
            resolution: `${layer.frameWidth}x${layer.frameHeight}`,
            fps: layer.framesPerSecond
         });
      });
   }
}, 10000);
```

## 优化媒体
<a name="web-publish-subscribe-optimizing-media"></a>

为了获得最佳性能，建议对 `getUserMedia` 和 `getDisplayMedia` 调用采取以下限制：

```
const CONSTRAINTS = {
    video: {
        width: { ideal: 1280 }, // Note: flip width and height values if portrait is desired
        height: { ideal: 720 },
        framerate: { ideal: 30 },
    },
};
```

您可以通过传递给 `LocalStageStream` 构造函数的附加选项进一步约束媒体：

```
const localStreamOptions = {
    minBitrate?: number;
    maxBitrate?: number;
    maxFramerate?: number;
    simulcast: {
        enabled: boolean
    }
}
const localStream = new LocalStageStream(track, localStreamOptions)
```

在以上代码中：
+ `minBitrate` 设置浏览器应使用的最小比特率。但是，低复杂度的视频流可能会导致编码器低于此比特率。
+ `maxBitrate` 设置浏览器不应超过的此流的最大比特率。
+ `maxFramerate` 设置浏览器不应超过的此流的最大帧率。
+ `simulcast` 选项仅在基于 Chromium 的浏览器上可用。它允许发送流的三个渲染层。
  + 这允许服务器根据网络限制选择发送给其他参与者的版本。
  + `simulcast` 与 `maxBitrate` 和/或 `maxFramerate` 值一起指定时，预计会根据这些值配置最高渲染层，前提是 `maxBitrate` 不低于内部 SDK 第二最高层 900 kbps 的默认 `maxBitrate` 值。
  + 如果与第二最高层的默认值相比，`maxBitrate` 被定过低，将禁用 `simulcast`。
  + 如果不通过让 `shouldPublishParticipant` 返回 `false`、调用 `refreshStrategy`、让 `shouldPublishParticipant` 返回 `true` 并再次调用 `refreshStrategy` 的组合操作来重新发布媒体，则无法打开和关闭 `simulcast`。

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

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

```
stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {
   console.log(`Participant ${participant.id} info:`, participant.attributes);
})
```

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

补充增强信息（SEI）NAL 单元用于在视频旁存储帧对齐的元数据。它可以在发布和订阅 H.264 视频流时使用。无法保证 SEI 有效载荷一定会到达订阅者手中，尤其是在网络条件不佳的情况下。由于 SEI 有效载荷直接在 H.264 帧结构中存储数据，因此这一功能无法用于纯音频流。

### 插入 SEI 有效载荷
<a name="sei-attributes-inserting-sei-payloads"></a>

发布客户端可以通过将其视频的 LocalStageStream 配置为启用 `inBandMessaging` 并随后调用 `insertSeiMessage` 方法来将 SEI 有效载荷插入正在发布的 Stage 流。请注意，启用 `inBandMessaging` 会增加 SDK 内存使用量。

有效载荷必须为 [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) 类型。有效载荷的大小必须大于 0 KB 且小于 1KB。每秒插入的 SEI 消息数量不得超过每秒 10KB。

```
const config = {
    inBandMessaging: { enabled: true }
};
const vidStream = new LocalStageStream(videoTrack, config);
const payload = new TextEncoder().encode('hello world').buffer;
vidStream.insertSeiMessage(payload);
```

#### 重复 SEI 有效载荷
<a name="sei-attributes-repeating-sei-payloads"></a>

可选择提供一个 `repeatCount`，以便在接下来发送的 N 个帧中重复插入 SEI 有效载荷。这将有助于减少因用于发送视频的底层 UDP 传输协议而可能造成的固有损耗。请注意，该值必须在 0 到 30 之间。接收客户端必须有删除重复消息的逻辑。

```
vidStream.insertSeiMessage(payload, { repeatCount: 5 }); // Optional config, repeatCount must be between 0 and 30
```

### 读取 SEI 有效载荷
<a name="sei-attributes-reading-sei-payloads"></a>

通过将订阅用户 `SubscribeConfiguration` 配置为启用 `inBandMessaging` 并侦听 `StageEvents.STAGE_STREAM_SEI_MESSAGE_RECEIVED` 事件，订阅客户端可以从发布 H.264 视频的发布者（如果存在）那里读取 SEI 有效载荷，如以下示例所示：

```
const strategy = {
    subscribeConfiguration: (participant) => {
        return {
            inBandMessaging: {
                enabled: true
            }
        }
    }
    // ... other strategy functions
}

stage.on(StageEvents.STAGE_STREAM_SEI_MESSAGE_RECEIVED, (participant, seiMessage) => {
    console.log(seiMessage.payload, seiMessage.uuid);
});
```

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

“联播分层编码”是一项 IVS 实时流媒体功能，允许发布者发送多个不同质量的视频层，也允许订阅用户动态或手动更改这些层。[直播优化](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/real-time-streaming-optimization.html)部分会对该功能作详细介绍。

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

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

```
// Enable Simulcast
let cameraStream = new LocalStageStream(cameraDevice, {
   simulcast: { enabled: true }
})
```

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

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

```
import { SimulcastLayerPresets } from ‘amazon-ivs-web-broadcast’

// Enable Simulcast
let cameraStream = new LocalStageStream(cameraDevice, {
   simulcast: {
      enabled: true,
      layers: [
         SimulcastLayerPresets.DEFAULT_720,
          SimulcastLayerPresets.DEFAULT_360,
          SimulcastLayerPresets.DEFAULT_180, 
   }
})
```

或者，您可以创建最多三层的自定义层配置。如果您提供空数组或不提供任何值，则使用上面描述的默认值。层通过以下必需属性进行描述：
+ `height: number;`
+ `width: number;`
+ `maxBitrateKbps: number;`
+ `maxFramerate: number;`

从预设开始，您可以覆盖单个属性，也可以创建全新的配置：

```
import { SimulcastLayerPresets } from ‘amazon-ivs-web-broadcast’

const custom720pLayer = {
   ...SimulcastLayerPresets.DEFAULT_720,
   maxFramerate: 15,
}

const custom360pLayer = {
       maxBitrateKbps: 600,
       maxFramerate: 15,
       width: 640,
       height: 360,
}

// Enable Simulcast
let cameraStream = new LocalStageStream(cameraDevice, {
   simulcast: {
      enabled: true,
      layers: [
         custom720pLayer,
         custom360pLayer, 
   }
})
```

有关配置单个层时可能触发的最大值、限制和错误，请参阅 SDK 参考文档。

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

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

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

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

使用 `subscribeConfiguration` 策略可以选择作为订阅用户要接收的初始层：

```
const strategy = {
    subscribeConfiguration: (participant) => {
        return {
            simulcast: {
                initialLayerPreference: InitialLayerPreference.LOWEST_QUALITY
            }
        }
    }
    // ... other strategy functions
}
```

默认情况下，系统总是先向订阅用户发送质量最低的层，而后慢慢增加到质量最高的层。这可以优化最终用户的带宽消耗，提供最佳的视频播放时间，从而减少网络较弱的用户的初始视频冻结。

以下选项适用于 `InitialLayerPreference`：
+ `LOWEST_QUALITY`：服务器首先会提供质量最低的视频层。这会优化带宽消耗以及媒体播放时间。质量定义为视频大小、比特率和帧速率的组合。例如，720p 视频的质量低于 1080p 视频的质量。
+ `HIGHEST_QUALITY`：服务器首先会提供质量最高的视频层。这会优化质量，也可能会增加媒体播放时间。质量定义为视频大小、比特率和帧速率的组合。例如，1080p 视频的质量优于 720p 视频的质量。

**注意：**要使初始层首选项（`initialLayerPreference` 调用）生效，必须重新订阅，因为这些更新不适用于有效订阅。



### 选项 2：首选直播层
<a name="web-layered-encoding-simulcast-preferred-layer"></a>

直播开始后，您可以使用 `preferredLayerForStream ` 策略方法。这种策略方法会公开参与者和直播信息。

该策略方法可以返回以下内容：
+ 直接基于 `RemoteStageStream.getLayers` 返回内容的层对象 
+ 基于 `StageStreamLayer.label` 的层对象标签字符串
+ “未定义”或 null，表示不应选择任何层，优先选择动态自适应

例如，以下策略会始终让用户选择质量最低的可用视频层：

```
const strategy = {
    preferredLayerForStream: (participant, stream) => {
        return stream.getLowestQualityLayer();
    }
    // ... other strategy functions
}
```

要重置层选择并返回动态自适应，则在策略中返回 null 或“未定义”。在本示例中，`appState` 是虚拟变量，表示可能的应用程序状态。

```
const strategy = {
    preferredLayerForStream: (participant, stream) => {
        if (appState.isAutoMode) {
            return null;
        } else {
            return appState.layerChoice
        }
    }
    // ... other strategy functions
}
```

### 选项 3：RemoteSageStream 层帮助程序
<a name="web-layered-encoding-simulcast-remotestagestream-helpers"></a>

`RemoteStageStream` 有几种帮助程序，可用于做出有关层选择的决定并向最终用户显示相应的选择：
+ **层事件**：除了 `RemoteStageStream` 之外，`StageEvents` 对象本身还有传达层和联播自适应变更的事件：
  + `stream.on(RemoteStageStreamEvents.ADAPTION_CHANGED, (isAdapting) => {})`
  + `stream.on(RemoteStageStreamEvents.LAYERS_CHANGED, (layers) => {})`
  + `stream.on(RemoteStageStreamEvents.LAYER_SELECTED, (layer, reason) => {})`
+ **层方法**：`RemoteStageStream` 有几种帮助程序方法，可用于获取有关流和正在呈现之层的信息。这些方法适用于 `preferredLayerForStream ` 策略中提供的远程流，以及通过 `StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED` 公开的远程流。
  + `stream.getLayers`
  + `stream.getSelectedLayer`
  + `stream.getLowestQualityLayer`
  + `stream.getHighestQualityLayer`

有关详细信息，请参阅 [SDK 参考文档](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference)中的 `RemoteStageStream` 类。出于 `LAYER_SELECTED` 原因，如果返回 `UNAVAILABLE`，则表示无法选择请求的层。尽量在其所在位置选择，通常是质量较低的层，以保持流稳定性。

## 处理网络问题
<a name="web-publish-subscribe-network-issues"></a>

本地设备的网络连接中断时，SDK 会内部尝试重新连接，无需用户执行任何操作。在某些情况下，SDK 无法重新连接，则需要用户操作。

一般来说，可以通过 `STAGE_CONNECTION_STATE_CHANGED` 事件来处理舞台状态：

```
stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {
   switch (state) {
      case StageConnectionState.DISCONNECTED:
         // handle disconnected UI
         return;
      case StageConnectionState.CONNECTING:
         // handle establishing connection UI
         return;
      case StageConnectionState.CONNECTED:
         // SDK is connected to the Stage
         return;
      case StageConnectionState.ERRORED:
         // SDK encountered an error and lost its connection to the stage. Wait for CONNECTED.
         return;
    }
})
```

通常，您可以忽略成功加入暂存区后遇到的错误状态，因为 SDK 将尝试在内部恢复。如果 SDK 报告 `ERRORED` 状态，并且该暂存区在很长一段时间（例如 30 秒或更长时间）内保持 `CONNECTING` 状态，则您可能已断开与网络的连接。

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

要广播舞台，请创建一个单独的 `IVSBroadcastClient` 会话，然后按照上述用 SDK 进行广播的常规说明进行操作。通过 `STAGE_PARTICIPANT_STREAMS_ADDED` 公开的 `StageStream` 列表可用于检索参与者媒体流，这些媒体流可以应用于广播流的构成，如下所示：

```
// Setup client with preferred settings
const broadcastClient = getIvsBroadcastClient();

stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => {
    streams.forEach(stream => {
        const inputStream = new MediaStream([stream.mediaStreamTrack]);
        switch (stream.streamType) {
            case StreamType.VIDEO:
                broadcastClient.addVideoInputDevice(inputStream, `video-${participant.id}`, {
                    index: DESIRED_LAYER,
                    width: MAX_WIDTH,
                    height: MAX_HEIGHT
                });
                break;
            case StreamType.AUDIO:
                broadcastClient.addAudioInputDevice(inputStream, `audio-${participant.id}`);
                break;
        }
    })
})
```

或者，您可以合成舞台并将其广播到 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)。

# IVS Web 广播 SDK 中的已知问题和解决方法 \$1 实时直播功能
<a name="broadcast-web-known-issues"></a>

本文档列出在使用 Amazon IVS 实时直播功能 Web 广播 SDK 时可能遇到的已知问题，并提出可能的建议解决方法。
+ 在不调用 `stage.leave()` 的情况下关闭浏览器标签页或退出浏览器时，用户仍然会出现在会话中，伴有长达 10 秒的静帧或黑屏。

  **解决办法**：尚无。
+ 对于会话开始后的用户加入，Safari 会话会间歇性出现黑屏。

  **解决方法：**刷新浏览器并重新连接会话。
+ Safari 无法从切换网络中正常恢复。

  **解决方法：**刷新浏览器并重新连接会话。
+ 开发人员控制台重复出现 `Error: UnintentionalError at StageSocket.onClose` 错误。

  **解决方法：**每个参与者令牌只能创建一个舞台。当使用相同的参与者令牌创建多个 `Stage` 实例时，无论该实例位于一台设备上还是在多台设备上，都会发生此错误。
+ 您可能无法维持 `StageParticipantPublishState.PUBLISHED` 状态，并且在侦听 `StageEvents.STAGE_PARTICIPANT_PUBLISH_STATE_CHANGED` 事件时可能会收到重复的 `StageParticipantPublishState.ATTEMPTING_PUBLISH` 状态。

  **解决办法：**调用 `getUserMedia` 或 `getDisplayMedia` 时，将视频分辨率限制为 720p。具体而言，`getUserMedia` 和 `getDisplayMedia` 的宽度和高度限制值相乘不得超过 921600（1280\$1720）。
+ 当调用 `stage.leave()` 或远程参与者离开时，浏览器的调试控制台中会出现 404 DELETE 错误。

  **解决办法**：尚无。这是一个无害的错误。

## Safari 限制
<a name="broadcast-web-safari-limitations"></a>
+ 拒绝权限提示需要在操作系统级别重置 Safari 网站设置中的权限。
+ Safari 本身无法像 Firefox 或 Chrome 那样有效地检测所有设备。例如，未检测到 OBS 虚拟摄像头。

## Firefox 限制
<a name="broadcast-web-firefox-limitations"></a>
+ Firefox 屏幕共享需要启用系统权限。启用后，用户必须重新启动 Firefox 才能正常运行；否则，如果认为权限被阻止，浏览器将抛出 [NotFoundError](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia#exceptions) 异常。
+ 缺少 `getCapabilities` 方法。这意味着用户无法获得媒体轨道的分辨率或宽高比。请参阅此 [bugzilla 主题帖](https://bugzilla.mozilla.org/show_bug.cgi?id=1179084)。
+ 缺少几个 `AudioContext` 属性；例如，延迟和通道数。这可能会给想要操作音轨的高级用户带来问题。
+ 在 MacOS 上，来自 `getUserMedia` 的摄像头画面被限制为 4:3 的宽高比。请参阅 [bugzilla 主题帖 1](https://bugzilla.mozilla.org/show_bug.cgi?id=1193640) 和 [bugzilla 主题帖 2](https://bugzilla.mozilla.org/show_bug.cgi?id=1306034)。
+ `getDisplayMedia` 不支持音频捕获。请参阅此 [bugzilla 主题帖](https://bugzilla.mozilla.org/show_bug.cgi?id=1541425)。
+ 屏幕捕获的帧率不理想（大约 15fps？）。请参阅此 [bugzilla 主题帖](https://bugzilla.mozilla.org/show_bug.cgi?id=1703522)。

## 移动 Web 限制
<a name="broadcast-web-mobile-web-limitations"></a>
+ 移动设备不支持 [getDisplayMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia#browser_compatibility) 屏幕共享。

  **解决办法**：尚无。
+ 在不调用 `leave()` 的情况下关闭浏览器时，参与者需要花 15-30 秒才能离开。

  **解决办法**：增加一个鼓励用户正确断开连接的 UI。
+ 后台应用程序会导致发布视频的操作停止。

  **解决办法**：在发布者暂停时显示 UI 列表。
+ 在 Android 设备上取消相机静音后，视频帧率会下降大约 5 秒。

  **解决办法**：尚无。
+ 对于 iOS 16.0，视频源在旋转时会被拉长。

  **解决办法**：显示概述此已知操作系统问题的 UI。
+ 切换音频输入设备会自动切换音频输出设备。

  **解决办法**：尚无。
+ 将浏览器置于后台运行会导致发布流变黑并仅产生音频。

  **解决办法**：尚无。这是出于安全原因。

# IVS Web 广播 SDK 中的错误处理 \$1 实时直播功能
<a name="broadcast-web-error-handling"></a>

本节概述错误条件、Web 广播 SDK 如何向应用程序报告错误条件，以及在遇到这些错误时应用程序应采取的措施。SDK 会向 `StageEvents.ERROR` 事件的侦听器报告错误：

```
stage.on(StageEvents.ERROR, (error: StageError) => {
    // log or handle errors here
    console.log(`${error.code}, ${error.category}, ${error.message}`);
});
```

## 暂存区错误
<a name="web-error-handling-stage-errors"></a>

当 SDK 遇到无法恢复的问题时会报告 StageError，通常需要应用程序干预和/或网络重新连接才能恢复。

每个报告的 `StageError` 都有一个代码（或 `StageErrorCode`）、消息（字符串）和类别（`StageErrorCategory`）。每个都与一个底层操作类别相关。

错误的操作类别根据其是否与暂存区的连接（`JOIN_ERROR`）、向暂存区发送媒体（`PUBLISH_ERROR`）或接收来自暂存区的传入媒体流（`SUBSCRIBE_ERROR`）有关来确定。

`StageError` 的代码属性报告特定问题：


| 名称 | 代码 | Recommended Action（建议的操作） | 
| --- | --- | --- | 
| TOKEN\$1MALFORMED | 1 | 创建一个有效的令牌，然后重试实例化暂存区。 | 
| TOKEN\$1EXPIRED | 2 | 创建一个未过期的令牌，然后重试实例化暂存区。 | 
| TIMEOUT | 3 | 操作已超时。如果暂存区存在且该令牌有效，则此失败很可能是网络问题。在这种情况下，等待设备连接恢复。 | 
| FAILED | 4 | 尝试操作时遇到致命情况。查看错误详细信息。 如果暂存区存在且该令牌有效，则此失败很可能是网络问题。在这种情况下，等待设备连接恢复。 对于大多数与网络稳定性相关的故障，SDK 将在发出 FAILED 错误之前在内部重试最多 30 秒。 | 
| CANCELED | 5 | 检查应用程序代码并确保没有重复的 `join`、`refreshStrategy` 或 `replaceStrategy` 调用，这些调用可能导致在完成之前开始和取消重复的操作。 | 
| STAGE\$1AT\$1CAPACITY | 6 | 此错误表明该暂存区或您的账户已满容量。如果该暂存区已达到参与者限制，则当该暂存区不再满容量时，请通过刷新策略再次尝试该操作。如果账户已达到其并发订阅量或并发发布者配额，请通过 [AWS 服务配额控制台](https://console.aws.amazon.com/servicequotas/)减少使用量或申请增加配额。 | 
| CODEC\$1MISMATCH | 7 | 暂存区不支持该编解码器。检查浏览器和平台是否支持编解码器。对于 IVS 实时直播功能，浏览器必须支持视频的 H.264 编解码器和音频的 Opus 编解码器。 | 
| TOKEN\$1NOT\$1ALLOWED | 8 | 令牌没有执行该操作的权限。使用正确的权限重新创建令牌，然后重试。 | 
| STAGE\$1DELETED | 9 | 无；尝试加入已删除的暂存区会触发此错误。 | 
| PARTICIPANT\$1DISCONNECTED | 10 | 无；尝试使用已断开连接的参与者的令牌加入会触发此错误。 | 

### 处理 StageError 示例
<a name="web-error-handling-stage-errors-example"></a>

使用 StageError 代码确定错误是否由于令牌过期导致：

```
stage.on(StageEvents.ERROR, (error: StageError) => {
    if (error.code === StageError.TOKEN_EXPIRED) {
        // recreate the token and stage instance and re-join
    }
});
```

### 已加入时出现网络错误
<a name="web-error-handling-stage-errors-network"></a>

如果设备的网络连接中断，SDK 可能会失去与暂存区服务器的连接。您可能会在控制台中看到错误，因为 SDK 无法再访问后端服务。向 https://broadcast.stats.live-video.net 执行 POST 操作会失败。

如果您正在发布和/或订阅，您将在控制台中看到与尝试发布/订阅相关的错误。

在内部，SDK 将尝试使用指数回退策略重新连接。

**操作**：等待设备连接恢复。

## 出错的状态
<a name="web-error-handling-errored-states"></a>

建议您使用这些状态进行应用程序日志记录，并向用户显示消息，提醒他们注意特定参与者与暂存区的连接问题。

### 发布
<a name="errored-states-publish"></a>

当发布失败时，SDK 会报告 `ERRORED`。

```
stage.on(StageEvents.STAGE_PARTICIPANT_PUBLISH_STATE_CHANGED, (participantInfo, state) => {
  if (state === StageParticipantPublishState.ERRORED) {
      // Log and/or display message to user
  }
});
```

### Subscribe
<a name="errored-states-subscribe"></a>

SDK 会在订阅失败时报告 `ERRORED`。这可能是由于网络条件或订阅者的某个阶段已满负荷而发生的情况。

```
stage.on(StageEvents.STAGE_PARTICIPANT_SUBSCRIBE_STATE_CHANGED, (participantInfo, state) => {
  if (state === StageParticipantSubscribeState.ERRORED) {
    // Log and/or display message to user
  }
});
```

# IVS 广播 SDK：Android 指南 \$1 实时直播功能
<a name="broadcast-android"></a>

IVS 实时流式传输 Android 广播 SDK 可让参与者在 Android 设备上发送和接收视频。

`com.amazonaws.ivs.broadcast` 软件包实现了本文档中所描述的接口。SDK 支持以下操作：
+ 加入舞台 
+ 向舞台中的其他参与者发布媒体
+ 舞台中其他参与者订阅媒体
+ 管理和监控发布到舞台的视频和音频
+ 获取每个对等连接的 WebRTC 统计信息
+ 所有操作均来自 IVS 低延迟流式传输 Android 广播 SDK

**Android 广播 SDK 的最新版本：**1.40.0（[发布说明](https://docs.aws.amazon.com/ivs/latest/RealTimeUserGuide/release-notes.html#mar12-26-broadcast-android-rt)） 

**参考文档：**有关 Amazon IVS Android 广播开发工具包中最重要方法的信息，请参阅参考文档，网址为 [https://aws.github.io/amazon-ivs-broadcast-docs/1.40.0/android/](https://aws.github.io/amazon-ivs-broadcast-docs/1.40.0/android/)。

**示例代码：**请参阅 GitHub 上的 Android 示例存储库：[https://github.com/aws-samples/amazon-ivs-real-time-streaming-android-samples](https://github.com/aws-samples/amazon-ivs-real-time-streaming-android-samples)。

**平台要求：**Android 9.0\$1

# IVS Android 广播 SDK 入门 \$1 实时直播功能
<a name="broadcast-android-getting-started"></a>

本文档将引导您完成 IVS 实时直播 Android 广播 SDK 入门所涉及的步骤。

## 安装库
<a name="broadcast-android-install"></a>

可通过多种方式将 Amazon IVS Android 广播库添加到您的 Android 开发环境：直接使用 Gradle、使用 Gradle 版本目录或手动安装 SDK。

**直接使用 Gradle**：将库添加到模块的 `build.gradle` 文件中，如此处所示（适用于最新版本的 IVS 广播 SDK）：

```
repositories {
    mavenCentral()
}
 
dependencies {
     implementation 'com.amazonaws:ivs-broadcast:1.40.0:stages@aar'
}
```

**使用 Gradle 版本目录**：首先将其包含在模块的 `build.gradle` 文件中：

```
implementation(libs.ivs){
   artifact {
      classifier = "stages"
      type = "aar"
   }
}
```

然后在 `libs.version.toml` 文件中加入以下内容（适用于最新版本的 IVS 广播 SDK）：

```
[versions]
ivs="1.40.0"

[libraries]
ivs = {module = "com.amazonaws:ivs-broadcast", version.ref = "ivs"}
```

**手动安装 SDK**：从以下位置下载最新版本：

[https://search.maven.org/artifact/com.amazonaws/ivs-broadcast](https://search.maven.org/artifact/com.amazonaws/ivs-broadcast)

请务必下载附加了 `-stages` 的 `aar`。

**同时允许 SDK 控制对讲电话**：无论选择哪种安装方法，都要在清单中添加以下权限，以允许 SDK 启用和禁用对讲电话：

```
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
```

## 使用带有调试符号的 SDK
<a name="broadcast-android-using-debug-symbols-rt"></a>

我们还发布了包含调试符号的 Android 广播 SDK 版本。如果您在 IVS 广播 SDK 中遇到崩溃，则可以使用此版本来提高 Firebase Crashlytics 中调试报告（堆栈跟踪）的质量；即 `libbroadcastcore.so`。当您向 IVS SDK 团队报告这些崩溃时，堆栈跟踪质量越高，修复问题越轻松。

要使用此版本的 SDK，请将以下内容放入您的 Gradle 构建文件中：

```
implementation "com.amazonaws:ivs-broadcast:$version:stages-unstripped@aar"
```

使用上面一行代替以下一行：

```
implementation "com.amazonaws:ivs-broadcast:$version:stages@aar"
```

### 将符号上传到 Firebase Crashlytics
<a name="android-debug-symbols-rt-firebase-crashlytics"></a>

确保已为 Firebase Crashlytics 设置 Gradle 构建文件。请按照此处的 Google 说明进行操作：

[https://firebase.google.com/docs/crashlytics/ndk-reports](https://firebase.google.com/docs/crashlytics/ndk-reports)

请务必将 `com.google.firebase:firebase-crashlytics-ndk` 作为依赖项包括在内。

在构建要发布的应用程序时，Firebase Crashlytics 插件应自动上传符号。要手动上传符号，请运行以下命令之一：

```
gradle uploadCrashlyticsSymbolFileRelease
```

```
./gradlew uploadCrashlyticsSymbolFileRelease
```

[如果符号上传两次（自动和手动上传）也无妨。]

### 防止您的版本 .apk 变得越来越大
<a name="android-debug-symbols-rt-sizing-apk"></a>

在打包版本 `.apk` 文件之前，Android Gradle 插件会自动尝试从共享库（包括 IVS 广播 SDK 的 `libbroadcastcore.so` 库）中剥离调试信息。但是，有时这种情况不会发生。因此，您的 `.apk` 文件可能会变大，您可能会收到来自 Android Gradle 插件的警告消息，告知无法剥离调试符号并将按原样打包 `.so` 文件。如果发生这种情况，则请执行以下操作：
+ 安装 Android NDK。任何最新版本都可以使用。
+ 将 `ndkVersion <your_installed_ndk_version_number>` 添加到应用程序的 `build.gradle` 文件中。即使您的应用程序本身不包含原生代码，也要这样做。

有关更多信息，请参阅此[问题报告](https://issuetracker.google.com/issues/353554169)。

## 请求权限
<a name="broadcast-android-permissions"></a>

您的应用必须请求权限才能访问用户摄像头和麦克风。（这并非特定于 Amazon IVS；需要访问摄像头和麦克风的任何应用程序都需要这样做。）

我们在此处检查用户是否已授予权限，如果没有，对他们提出要求：

```
final String[] requiredPermissions =
         { Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO };

for (String permission : requiredPermissions) {
    if (ContextCompat.checkSelfPermission(this, permission) 
                != PackageManager.PERMISSION_GRANTED) {
        // If any permissions are missing we want to just request them all.
        ActivityCompat.requestPermissions(this, requiredPermissions, 0x100);
        break;
    }
}
```

在这里，我们得到用户的响应：

```
@Override
public void onRequestPermissionsResult(int requestCode, 
                                      @NonNull String[] permissions,
                                      @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode,
               permissions, grantResults);
    if (requestCode == 0x100) {
        for (int result : grantResults) {
            if (result == PackageManager.PERMISSION_DENIED) {
                return;
            }
        }
        setupBroadcastSession();
    }
}
```

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

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

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

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

### 舞台
<a name="android-publish-subscribe-concepts-stage"></a>

`Stage` 类是主机应用程序和 SDK 之间交互的主要点。此类表示舞台，用于加入和退出舞台。创建和加入舞台需要控制面板上有效的未过期令牌字符串（表示为 `token`）。加入和退出舞台很简单。

```
Stage stage = new Stage(context, token, strategy);

try {
	stage.join();
} catch (BroadcastException exception) {
	// handle join exception
}

stage.leave();
```

也可以将 `StageRenderer` 附加到 `Stage` 类：

```
stage.addRenderer(renderer); // multiple renderers can be added
```

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

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

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

```
Stage.SubscribeType shouldSubscribeToParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo);
```

当远程参与者加入舞台，SDK 会向主机应用程序查询该参与者的所需订阅状态。选项为 `NONE`、`AUDIO_ONLY` 和 `AUDIO_VIDEO`。为该函数返回值时，主机应用程序无需担心发布状态、当前订阅状态或舞台连接状态。如果返回 `AUDIO_VIDEO`，则 SDK 会等到远程参与者发布后再订阅，并在整个过程中通过渲染器更新主机应用程序。

以下是实施示例：

```
@Override
Stage.SubscribeType shouldSubscribeToParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	return Stage.SubscribeType.AUDIO_VIDEO;
}
```

完整实施此功能，适用于始终希望所有参与者都能看到对方的主机应用程序；例如，视频聊天应用程序。

也可以进行更高级的实施。根据服务器提供的属性，使用 `ParticipantInfo` 上的 `userInfo` 属性有选择地订阅参与者：

```
@Override
Stage.SubscribeType shouldSubscribeToParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	switch(participantInfo.userInfo.get(“role”)) {
		case “moderator”:
			return Stage.SubscribeType.NONE;
		case “guest”:
			return Stage.SubscribeType.AUDIO_VIDEO;
		default:
			return Stage.SubscribeType.NONE;
	}
}
```

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

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

```
SubscribeConfiguration subscribeConfigurationForParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo);
```

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

以下是一个实现示例：

```
@Override
public SubscribeConfiguration subscribeConfigrationForParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
    SubscribeConfiguration config = new SubscribeConfiguration();

    config.jitterBuffer.setMinDelay(JitterBufferConfiguration.JitterBufferDelay.MEDIUM());

    return config;
}
```

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

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

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

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

```
boolean shouldPublishFromParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo);
```

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

以下是实施示例：

```
@Override
boolean shouldPublishFromParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	return true;
}
```

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

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

```
@Override
List<LocalStageStream> stageStreamsToPublishForParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo);
}
```

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

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

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

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

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

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

`StageRenderer` 接口将舞台状态传递给主机应用程序。渲染器提供的事件通常完全可以支持主机应用程序界面的更新。渲染器提供以下函数：

```
void onParticipantJoined(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo);

void onParticipantLeft(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo);

void onParticipantPublishStateChanged(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull Stage.PublishState publishState);

void onParticipantSubscribeStateChanged(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull Stage.SubscribeState subscribeState);

void onStreamsAdded(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull List<StageStream> streams);

void onStreamsRemoved(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull List<StageStream> streams);

void onStreamsMutedChanged(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull List<StageStream> streams);

void onError(@NonNull BroadcastException exception);

void onConnectionStateChanged(@NonNull Stage stage, @NonNull Stage.ConnectionState state, @Nullable BroadcastException exception);
                
void onStreamAdaptionChanged(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull RemoteStageStream stream, boolean adaption);

void onStreamLayersChanged(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull RemoteStageStream stream, @NonNull List<RemoteStageStream.Layer> layers);

void onStreamLayerSelected(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull RemoteStageStream stream, @Nullable RemoteStageStream.Layer layer, @NonNull RemoteStageStream.LayerSelectedReason reason);
```

对于其中大多数方法，提供相应的 `Stage` 和 `ParticipantInfo`。

预计渲染器提供的信息不会影响策略的返回值。例如，调用 `onParticipantPublishStateChanged` 时，`shouldSubscribeToParticipant` 的返回值预计不会改变。如果主机应用程序想要订阅特定参与者，则无论该参与者的发布状态如何，它都应返回所需的订阅类型。SDK 负责确保根据舞台状态在正确的时间执行策略的期望状态。

可以将 `StageRenderer` 附加到舞台类：

```
stage.addRenderer(renderer); // multiple renderers can be added
```

请注意，只有发布参与者才会触发 `onParticipantJoined`，每当参与者停止发布或退出舞台会话时，都会触发 `onParticipantLeft`。

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

通过 `DeviceDiscovery` 发现内置麦克风和摄像头等本地设备。以下示例演示如何选择前置摄像头和默认麦克风，然后将它们作为 `LocalStageStreams` 返回，由 SDK 发布：

```
DeviceDiscovery deviceDiscovery = new DeviceDiscovery(context);

List<Device> devices = deviceDiscovery.listLocalDevices();
List<LocalStageStream> publishStreams = new ArrayList<LocalStageStream>();

Device frontCamera = null;
Device microphone = null;

// Create streams using the front camera, first microphone
for (Device device : devices) {
	Device.Descriptor descriptor = device.getDescriptor();
	if (!frontCamera && descriptor.type == Device.Descriptor.DeviceType.Camera && descriptor.position = Device.Descriptor.Position.FRONT) {
		front Camera = device;
	}
	if (!microphone && descriptor.type == Device.Descriptor.DeviceType.Microphone) {
		microphone = device;
	}
}

ImageLocalStageStream cameraStream = new ImageLocalStageStream(frontCamera);
AudioLocalStageStream microphoneStream = new AudioLocalStageStream(microphoneDevice);

publishStreams.add(cameraStream);
publishStreams.add(microphoneStream);

// Provide the streams in Stage.Strategy
@Override
@NonNull List<LocalStageStream> stageStreamsToPublishForParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	return publishStreams;
}
```

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

订阅完成后，您将通过渲染器的 `onStreamsAdded` 函数接收一组 `StageStream` 对象。您可以从 `ImageStageStream` 检索预览：

```
ImagePreviewView preview = ((ImageStageStream)stream).getPreview();

// Add the view to your view hierarchy
LinearLayout previewHolder = findViewById(R.id.previewHolder);
preview.setLayoutParams(new LinearLayout.LayoutParams(
		LinearLayout.LayoutParams.MATCH_PARENT,
		LinearLayout.LayoutParams.MATCH_PARENT));
previewHolder.addView(preview);
```

您可以从 `AudioStageStream` 检索音频级别的统计信息：

```
((AudioStageStream)stream).setStatsCallback((peak, rms) -> {
	// handle statistics
});
```

当参与者停止发布或取消订阅时，将使用已删除的流来调用 `onStreamsRemoved` 函数。主机应用程序应将其用作信号，从视图层次结构中删除参与者的视频流。

在所有可能删除流的场景中都会调用 `onStreamsRemoved`，包括：
+ 远程参与者停止发布。
+ 本地设备取消订阅或将订阅从 `AUDIO_VIDEO` 更改为 `AUDIO_ONLY`。
+ 远程参与者退出舞台。
+ 本地参与者退出舞台。

由于在所有场景中都会调用 `onStreamsRemoved`，因此在远程或本地退出操作期间，从用户界面中删除参与者无需自定义业务逻辑。

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

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

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

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

当参与者更改其视频或音频流的静音状态时，将使用已更改的流列表调用渲染器 `onStreamMutedChanged` 函数。使用 `StageStream` 上的 `getMuted` 方法相应地更新您的用户界面。

```
@Override
void onStreamsMutedChanged(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull List<StageStream> streams) {
	for (StageStream stream : streams) {
		boolean muted = stream.getMuted();
		// handle UI changes
	}
}
```

## 获取 WebRTC 统计信息
<a name="android-publish-subscribe-webrtc-stats"></a>

要获取发布流或订阅流的最新 WebRTC 统计信息，请使用 `StageStream` 上的 `requestRTCStats`。收集完成后，您将通过 `StageStream.Listener`（可在 `StageStream` 上设置）收到统计信息。

```
stream.requestRTCStats();

@Override
void onRTCStats(Map<String, Map<String, String>> statsMap) {
	for (Map.Entry<String, Map<String, string>> stat : statsMap.entrySet()) {
		for(Map.Entry<String, String> member : stat.getValue().entrySet()) {
			Log.i(TAG, stat.getKey() + “ has member “ + member.getKey() + “ with value “ + member.getValue());
		}
	}
}
```

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

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

```
@Override
void onParticipantJoined(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	for (Map.Entry<String, String> entry : participantInfo.userInfo.entrySet()) {
		Log.i(TAG, “attribute: “ + entry.getKey() + “ = “ + entry.getValue());
	}
}
```

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

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

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

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

发布客户端可以使用 ImageDevice 上的 `embedMessage` 方法将消息有效载荷嵌入到其视频流中。有效载荷的大小必须大于 0 KB 且小于 1KB。每秒插入的嵌入式消息数量不得超过每秒 10KB。

```
val surfaceSource: SurfaceSource = imageStream.device as SurfaceSource
val message = "hello world"
val messageBytes = message.toByteArray(StandardCharsets.UTF_8)

try {
    surfaceSource.embedMessage(messageBytes, 0)
} catch (e: BroadcastException) {
    Log.e("EmbedMessage", "Failed to embed message: ${e.message}")
}
```

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

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

```
try {
    surfaceSource.embedMessage(messageBytes, 5)
    // repeatCount: 0-30, receiving clients should handle duplicates
} catch (e: BroadcastException) {
    Log.e("EmbedMessage", "Failed to embed message: ${e.message}")
}
```

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

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

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

补充增强信息（SEI）NAL 单元用于在视频旁存储帧对齐的元数据。订阅客户端通过检查从发布者的 `ImageDevice` 发出来的 `ImageDeviceFrame` 对象上的 `embeddedMessages` 属性，可以从发布 H.264 视频的发布者那里读取 SEI 有效载荷。为此，请获取发布者的 `ImageDevice`，然后通过提供给 `setOnFrameCallback` 的回调来观察每一帧，如下例所示：

```
// in a StageRenderer’s onStreamsAdded function, after acquiring the new ImageStream

val imageDevice = imageStream.device as ImageDevice
imageDevice.setOnFrameCallback(object : ImageDevice.FrameCallback {
	override fun onFrame(frame: ImageDeviceFrame) {
    		for (message in frame.embeddedMessages) {
        		if (message is UserDataUnregisteredSeiMessage) {
            		val seiMessageBytes = message.data
            		val seiMessageUUID = message.uuid
           	 
            		// interpret the message's data based on the UUID
        		}
    		}
	}
})
```

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

应用程序进入后台时，您可能需要停止发布或仅订阅其他远程参与者的音频。要实现此目的，请更新 `Strategy` 实施以停止发布，然后订阅 `AUDIO_ONLY`（或者 `NONE`，如果适用）。

```
// Local variables before going into the background
boolean shouldPublish = true;
Stage.SubscribeType subscribeType = Stage.SubscribeType.AUDIO_VIDEO;

// Stage.Strategy implementation
@Override
boolean shouldPublishFromParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	return shouldPublish;
}

@Override
Stage.SubscribeType shouldSubscribeToParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
	return subscribeType;
}

// In our Activity, modify desired publish/subscribe when we go to background, then call refreshStrategy to update the stage
@Override
void onStop() {
	super.onStop();
	shouldPublish = false;
	subscribeTpye = Stage.SubscribeType.AUDIO_ONLY;
	stage.refreshStrategy();
}
```

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

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

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

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

```
// Enable Simulcast
StageVideoConfiguration config = new StageVideoConfiguration();
config.simulcast.setEnabled(true);

ImageLocalStageStream cameraStream = new ImageLocalStageStream(frontCamera, config);

// Other Stage implementation code
```

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

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

```
// Enable Simulcast
StageVideoConfiguration config = new StageVideoConfiguration();
config.simulcast.setEnabled(true);

List<StageVideoConfiguration.Simulcast.Layer> simulcastLayers = new ArrayList<>();
simulcastLayers.add(StagePresets.SimulcastLocalLayer.DEFAULT_720);
simulcastLayers.add(StagePresets.SimulcastLocalLayer.DEFAULT_180);

config.simulcast.setLayers(simulcastLayers);

ImageLocalStageStream cameraStream = new ImageLocalStageStream(frontCamera, config);

// Other Stage implementation code
```

或者，您可以创建最多三层的自定义层配置。如果您提供空数组或不提供任何值，则使用上面描述的默认值。层通过以下必需的属性 setter 进行描述：
+ `setSize: Vec2;`
+ `setMaxBitrate: integer;`
+ `setMinBitrate: integer;`
+ `setTargetFramerate: integer;`

从预设开始，您可以覆盖单个属性，也可以创建全新的配置：

```
// Enable Simulcast
StageVideoConfiguration config = new StageVideoConfiguration();
config.simulcast.setEnabled(true);

List<StageVideoConfiguration.Simulcast.Layer> simulcastLayers = new ArrayList<>();

// Configure high quality layer with custom framerate
StageVideoConfiguration.Simulcast.Layer customHiLayer = StagePresets.SimulcastLocalLayer.DEFAULT_720;
customHiLayer.setTargetFramerate(15);

// Add layers to the list
simulcastLayers.add(customHiLayer);
simulcastLayers.add(StagePresets.SimulcastLocalLayer.DEFAULT_180);

config.simulcast.setLayers(simulcastLayers);

ImageLocalStageStream cameraStream = new ImageLocalStageStream(frontCamera, config);

// Other Stage implementation code
```

有关配置单个层时可能触发的最大值、限制和错误，请参阅 SDK 参考文档。

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

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

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

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

使用 `subscribeConfigurationForParticipant` 策略可以选择作为订阅用户要接收的初始层：

```
@Override
public SubscribeConfiguration subscribeConfigrationForParticipant(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo) {
    SubscribeConfiguration config = new SubscribeConfiguration();

    config.simulcast.setInitialLayerPreference(SubscribeSimulcastConfiguration.InitialLayerPreference.LOWEST_QUALITY);

    return config;
}
```

默认情况下，系统总是先向订阅用户发送质量最低的层，而后慢慢增加到质量最高的层。这可以优化最终用户的带宽消耗，提供最佳的视频播放时间，从而减少网络较弱的用户的初始视频冻结。

以下选项适用于 `InitialLayerPreference`：
+ `LOWEST_QUALITY`：服务器首先会提供质量最低的视频层。这会优化带宽消耗以及媒体播放时间。质量定义为视频大小、比特率和帧速率的组合。例如，720p 视频的质量低于 1080p 视频的质量。
+ `HIGHEST_QUALITY`：服务器首先会提供质量最高的视频层。这会优化质量，也可能会增加媒体播放时间。质量定义为视频大小、比特率和帧速率的组合。例如，1080p 视频的质量优于 720p 视频的质量。

**注意：**要使初始层首选项（`setInitialLayerPreference` 调用）生效，必须重新订阅，因为这些更新不适用于有效订阅。

### 选项 2：首选直播层
<a name="android-layered-encoding-simulcast-preferred-layer"></a>

`preferredLayerForStream` 策略方法可使您在直播开始后选择图层。此策略方法接收参与者和直播信息，因此您可以根据各个参与者选择图层。SDK 在特定事件发生时调用此方法，例如流图层变化、参与者状态更改或主机应用程序刷新策略时。

此策略方法返回 `RemoteStageStream.Layer` 以下对象之一：
+ 图层对象，比如 `RemoteStageStream.getLayers` 返回的图层对象。
+ null，表示不应选择任何层，优先选择动态自适应。

例如，以下策略会始终让用户选择质量最低的可用视频层：

```
@Nullable
@Override
public RemoteStageStream.Layer preferredLayerForStream(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull RemoteStageStream stream) {
    return stream.getLowestQualityLayer();
}
```

要重置层选择并返回动态自适应，则在策略中返回 null 或“未定义”。在本示例中，`appState` 是占位符变量，表示主机应用程序状态。

```
@Nullable
@Override
public RemoteStageStream.Layer preferredLayerForStream(@NonNull Stage stage, @NonNull ParticipantInfo participantInfo, @NonNull RemoteStageStream stream) {
    if (appState.isAutoMode) {
        return null;
    } else {
        return appState.layerChoice;
    }
}
```

### 选项 3：RemoteSageStream 层帮助程序
<a name="android-layered-encoding-simulcast-remotestagestream-helpers"></a>

`RemoteStageStream` 有几种帮助程序，可用于做出有关层选择的决定并向最终用户显示相应的选择：
+ **层事件**：除了 `RemoteStageStream.Listener` 之外，`StageRenderer` 还有传达层和联播自适应变更的事件：
  + `void onAdaptionChanged(boolean adaption)`
  + `void onLayersChanged(@NonNull List<Layer> layers)`
  + `void onLayerSelected(@Nullable Layer layer, @NonNull LayerSelectedReason reason)`
+ **层方法**：`RemoteStageStream` 有几种帮助程序方法，可用于获取有关流和正在呈现之层的信息。这些方法适用于 `preferredLayerForStream` 策略中提供的远程流，以及通过 `StageRenderer.onStreamsAdded` 公开的远程流。
  + `stream.getLayers`
  + `stream.getSelectedLayer`
  + `stream.getLowestQualityLayer`
  + `stream.getHighestQualityLayer`
  + `stream.getLayersWithConstraints`

有关详细信息，请参阅 [SDK 参考文档](https://aws.github.io/amazon-ivs-broadcast-docs/latest/android/)中的 `RemoteStageStream` 类。出于 `LayerSelected` 原因，如果返回 `UNAVAILABLE`，则表示无法选择请求的层。尽量在其所在位置选择，通常是质量较低的层，以保持流稳定性。

## 视频配置限制
<a name="android-publish-subscribe-video-limits"></a>

SDK 不支持使用 `StageVideoConfiguration.setSize(BroadcastConfiguration.Vec2 size)` 强制纵向模式或横向模式。在纵向中，较小的尺寸为宽度；在横向中，较小的尺寸为高度。这意味着以下两个对 `setSize` 的调用会对视频配置产生相同的影响：

```
StageVideo Configuration config = new StageVideo Configuration();

config.setSize(BroadcastConfiguration.Vec2(720f, 1280f);
config.setSize(BroadcastConfiguration.Vec2(1280f, 720f);
```

## 处理网络问题
<a name="android-publish-subscribe-network-issues"></a>

本地设备的网络连接中断时，SDK 会内部尝试重新连接，无需用户执行任何操作。在某些情况下，SDK 无法重新连接，则需要用户操作。有两个与网络连接中断有关的主要错误：
+ 错误代码 1400，消息：“由于未知的网络错误，PeerConnection 中断”
+ 错误代码 1300，消息：“重试次数已用完”

如果收到第一个错误但没有收到第二个错误，则 SDK 仍在连接该舞台，并将尝试自动重新建立连接。作为一种保护措施，您可以在不更改策略方法的返回值的情况下调用 `refreshStrategy`，以触发手动重新连接。

如果收到第二个错误，则 SDK 的重新连接尝试已失败，本地设备不再连接到舞台。在这种情况下，请尝试在重新建立网络连接后调用 `join`，以重新加入舞台。

通常，成功加入舞台后遇到错误则表明 SDK 未能成功重新建立连接。创建新的 `Stage` 对象，并在网络条件改善时尝试加入。

## 使用蓝牙麦克风
<a name="android-publish-subscribe-bluetooth-microphones"></a>

要使用蓝牙麦克风设备进行发布，必须启动蓝牙 SCO 连接：

```
Bluetooth.startBluetoothSco(context);
// Now bluetooth microphones can be used
…
// Must also stop bluetooth SCO
Bluetooth.stopBluetoothSco(context);
```

# IVS Android 广播 SDK 中的已知问题和解决方法 \$1 实时直播功能
<a name="broadcast-android-known-issues"></a>

本文档列出在使用 Amazon IVS 实时直播 Android 广播 SDK 时可能遇到的已知问题，并提出可能的建议解决方法。
+ 当 Android 设备进入睡眠状态然后唤醒时，预览可能会处于冻结状态。

  **解决方法：**创建并使用新的 `Stage`。
+ 当一个参与者使用另一个参与者正在使用的令牌加入时，第一个连接将断开，不会出现具体错误提示。

  **解决办法**：尚无。
+ 有一个问题比较少见，即发布者正在发布，但订阅用户收到的发布状态是 `inactive`。

  **解决方法：**尝试退出然后加入会话。如果问题仍然存在，请为发布者创建新令牌。
+ 在舞台会话期间，通常在持续时间较长的通话中，可能会间歇性地出现罕见的音频失真问题。

  **解决方法：**遇到音频失真问题的参与者可以退出并重新加入会话，也可以取消发布并重新发布音频，以修复问题。
+ 发布到舞台时不支持外接麦克风。

  **解决方法：**不要使用通过 USB 连接的外接麦克风发布到舞台。
+ 不支持使用 `createSystemCaptureSources` 屏幕共享发布到舞台。

  **解决方法：**使用自定义图像输入源和自定义音频输入源手动管理系统捕获。
+ 当从父级中删除 `ImagePreviewView` 时（例如，在父级调用 `removeView()`），会立即释放 `ImagePreviewView`。将其添加到另一个父视图时，`ImagePreviewView` 不显示任何帧。

  **解决方法：**使用 `getPreview` 请求再次预览。
+ 使用搭载 Android 12 的 Samsung Galaxy S22/\$1 加入舞台时，可能会遇到 1401 错误，显示本地设备无法加入舞台或加入舞台但没有音频。

  **解决方法：**升级到 Android 13。
+ 在 Android 13 上使用 Nokia X20 加入舞台时，可能无法打开相机并引发异常。

  **解决办法**：尚无。
+ 装有 MediaTek Helio 芯片组的设备可能无法正确渲染远程参与者的视频。

  **解决办法**：尚无。
+ 在少数设备上，设备操作系统选择的麦克风可能与通过 SDK 选择的麦克风不同。这是因为 Amazon IVS 广播 SDK 无法控制 `VOICE_COMMUNICATION` 音频路由的定义方式，因为定义方式因不同的设备制造商而异。

  **解决办法**：尚无。
+ 某些 Android 视频编码器不能配置小于 176x176 的视频大小。配置较小的大小会导致错误并阻止流式传输。

  **解决办法：**不要将视频大小配置为小于 176x176。

# IVS Android 广播 SDK 中的错误处理 \$1 实时直播功能
<a name="broadcast-android-error-handling"></a>

本节概述错误条件、IVS 实时直播 Android 广播 SDK 如何向应用程序报告错误条件，以及在遇到这些错误时应用程序应采取的措施。

## 致命错误与非致命错误
<a name="broadcast-android-fatal-vs-nonfatal-errors"></a>

错误对象带有值为 `BroadcastException` 的“is fatal”布尔字段。

通常，致命错误与阶段服务器的连接有关（连接无法建立，或者连接丢失且无法恢复）。应用程序应重新创建阶段并重新加入，可能使用新令牌或在设备连接恢复后重新加入。

非致命错误通常与发布/订阅状态有关，由 SDK 处理，SDK 会重试发布/订阅操作。

可以检查如下属性：

```
try {
  stage.join(...)
} catch (e: BroadcastException) {
  If (e.isFatal) { 
    // the error is fatal
```

## 加入错误
<a name="broadcast-android-stage-join-errors"></a>

### 令牌格式不正确
<a name="broadcast-android-stage-join-errors-malformed-token"></a>

当阶段令牌格式不正确时，就会发生这种情况。

SDK 在调用 `stage.join` 时引发 Java 异常，其中错误代码 = 1000，fatal = true。

**操作**：创建有效令牌并重试加入。

### 令牌已过期
<a name="broadcast-android-stage-join-errors-expired-token"></a>

当阶段令牌过期时，就会发生这种情况。

SDK 在调用 `stage.join` 时引发 Java 异常，其中错误代码 = 1001，fatal = true。

**操作**：创建新令牌并重试加入。

### 令牌无效或已撤销
<a name="broadcast-android-stage-join-errors-invalid-token"></a>

当阶段令牌没有格式错误但被阶段服务器拒绝时，就会发生这种情况。通过应用程序提供的阶段渲染器异步报告此情况。

SDK 调用 `onConnectionStateChanged` 时引发异常，其中错误代码 = 1026，fatal = true。

**操作**：创建有效令牌并重试加入。

### 初始加入时出现网络错误
<a name="broadcast-android-stage-join-errors-network-initial-join"></a>

当 SDK 无法联系阶段服务器建立连接时，就会发生这种情况。通过应用程序提供的阶段渲染器异步报告此情况。

SDK 调用 `onConnectionStateChanged` 时引发异常，其中错误代码 = 1300，fatal = true。

**操作**：等待设备连接恢复，然后重试加入。

### 已加入时出现网络错误
<a name="broadcast-android-stage-join-errors-network-already-joined"></a>

如果设备的网络连接中断，SDK 可能会失去与阶段服务器的连接。通过应用程序提供的阶段渲染器异步报告此情况。

SDK 调用 `onConnectionStateChanged` 时引发异常，其中错误代码 = 1300，fatal = true。

**操作**：等待设备连接恢复，然后重试加入。

## 发布/订阅错误
<a name="broadcast-android-publish-subscribe-errors"></a>

### 初次
<a name="broadcast-android-publish-subscribe-errors-initial"></a>

有如下几种错误：
+ MultihostSessionOfferCreationFailPublish（1020）
+ MultihostSessionOfferCreationFailSubscribe（1021）
+ MultihostSessionNoIceCandidates（1022）
+ MultihostSessionStageAtCapacity（1024）
+ SignallingSessionCannotRead（1201）
+ SignallingSessionCannotSend（1202）
+ SignallingSessionBadResponse（1203）

通过应用程序提供的阶段渲染器异步报告这些情况。

SDK 会在有限的次数内重试该操作。在重试期间，发布/订阅状态为 `ATTEMPTING_PUBLISH`/`ATTEMPTING_SUBSCRIBE`。如果重试成功，则状态将更改为 `PUBLISHED`/`SUBSCRIBED`。

SDK 调用 `onError` 时引发相关的错误代码，并且 fatal = false。

**操作**：无需执行任何操作，因为 SDK 会自动重试。或者，应用程序可以刷新策略以强制进行更多重试。

### 已经建立，然后失败
<a name="broadcast-android-publish-subscribe-errors-established"></a>

发布或订阅在建立后可能会失败，很可能是由于网络错误所致。“对等连接由于未知的网络错误中断”的错误代码为 1400。

通过应用程序提供的阶段渲染器异步报告此情况。

SDK 会重试发布/订阅操作。在重试期间，发布/订阅状态为 `ATTEMPTING_PUBLISH`/`ATTEMPTING_SUBSCRIBE`。如果重试成功，则状态将更改为 `PUBLISHED`/`SUBSCRIBED`。

SDK 调用 `onError` 时引发相关的错误，其中错误代码 = 1400，fatal = false。

**操作**：无需执行任何操作，因为 SDK 会自动重试。或者，应用程序可以刷新策略以强制进行更多重试。如果连接完全丢失，指向阶段的连接也可能失败。

# IVS 广播 SDK：iOS 指南 \$1 实时直播功能
<a name="broadcast-ios"></a>

IVS 实时流式传输 iOS 广播 SDK 让参与者能在 iOS 设备上发送和接收视频。

`AmazonIVSBroadcast` 模块实施了本文档中所描述的接口。支持以下操作：
+ 加入舞台 
+ 向舞台中的其他参与者发布媒体
+ 舞台中其他参与者订阅媒体
+ 管理和监控发布到舞台的视频和音频
+ 获取每个对等连接的 WebRTC 统计信息
+ 所有操作均来自 IVS 低延迟流式传输 iOS 广播 SDK

**iOS 广播 SDK 的最新版本：**1.40.0（[发布说明](https://docs.aws.amazon.com/ivs/latest/RealTimeUserGuide/release-notes.html#mar12-26-broadcast-ios-rt)） 

**参考文档：**有关 Amazon IVS iOS 广播开发工具包中最重要方法的信息，请参阅参考文档，网址为 [https://aws.github.io/amazon-ivs-broadcast-docs/1.40.0/ios/](https://aws.github.io/amazon-ivs-broadcast-docs/1.40.0/ios/)。

**示例代码：**请参阅 GitHub 上的 iOS 示例存储库：[https://github.com/aws-samples/amazon-ivs-real-time-streaming-ios-samples](https://github.com/aws-samples/amazon-ivs-real-time-streaming-ios-samples)。

**平台要求：**iOS 14\$1

# IVS iOS 广播 SDK 入门 \$1 实时直播功能
<a name="broadcast-ios-getting-started"></a>

本文档将引导您完成 IVS 实时直播 iOS 广播 SDK 入门所涉及的步骤。

## 安装库
<a name="broadcast-ios-install"></a>

我们建议您通过 Swift 程序包管理器集成广播 SDK。（或者，您可以手动将框架添加至项目。）

### 建议：集成广播 SDK（Swift 程序包管理器）
<a name="broadcast-ios-install-swift"></a>

1. 从 [https://broadcast.live-video.net/1.40.0/Package.swift](https://broadcast.live-video.net/1.40.0/Package.swift) 下载 Package.swift 文件。

1. 在您的项目中，创建一个名为 AmazonIVSBroadcast 的新目录并将其添加到版本控制中。

1. 将下载的 Package.swift 文件放到新目录中。

1. 在您的 Xcode 项目中，转到**文件 > 添加软件包依赖项**，然后选择**添加本地...**

1. 导航到并选择您创建的 AmazonIVSBroadcast 目录，然后选择**添加软件包**。

1. 当系统提示**选择 AmazonIVSBroadcast 的软件包产品**时，请通过在**添加到目标**部分中设置应用程序目标来将 **AmazonIVSBroadcastStages** 选为**软件包产品**。

1. 选择**添加软件包**。

**重要提示：**IVS 实时流式传输广播 SDK 包含 IVS 低延迟流式传输广播 SDK 的所有功能。不可能将两个 SDK 集成到同一个项目中。

### 替代方法：手动安装框架
<a name="broadcast-ios-install-manual"></a>

1. 最新版本下载链接：[https://broadcast.live-video.net/1.40.0/AmazonIVSBroadcast-Stages.xcframework.zip](https://broadcast.live-video.net/1.40.0/AmazonIVSBroadcast-Stages.xcframework.zip)。

1. 提取归档的内容。`AmazonIVSBroadcast.xcframework` 包含适用于设备和模拟器的开发工具包。

1. 通过以下方法嵌入 `AmazonIVSBroadcast.xcframework`：将其拖动到应用程序目标的 **General**（常规）选项卡中的 **Frameworks, Libraries, and Embedded Content**（框架、库和嵌入式内容）部分中。  
![\[应用程序目标 General（常规）选项卡上的 Frameworks, Libraries, and Embedded Content（框架、库和嵌入式内容）部分：\]](http://docs.aws.amazon.com/zh_cn/ivs/latest/RealTimeUserGuide/images/iOS_Broadcast_SDK_Guide_xcframework.png)

## 请求权限
<a name="broadcast-ios-permissions"></a>

您的应用必须请求权限才能访问用户摄像头和麦克风。（这并非特定于 Amazon IVS；需要访问摄像头和麦克风的任何应用程序都需要这样做。）

我们在此处检查用户是否已授予权限，如果没有，对他们提出要求：

```
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized: // permission already granted.
case .notDetermined:
   AVCaptureDevice.requestAccess(for: .video) { granted in
       // permission granted based on granted bool.
   }
case .denied, .restricted: // permission denied.
@unknown default: // permissions unknown.
}
```

如果您希望分别访问摄像头和麦克风，则需要对 `.video` 和 `.audio` 媒体类型进行此操作。

您还需要将 `NSCameraUsageDescription` 和 `NSMicrophoneUsageDescription` 的条目添加到 `Info.plist`。否则，尝试请求权限时，您的应用程序将崩溃。

## 禁用应用程序空闲计时器
<a name="broadcast-ios-disable-idle-timer"></a>

您可以自由选择，但我们建议您这样做。它可以防止您的设备在使用广播开发工具包时进入睡眠状态，这会中断广播。

```
override func viewDidAppear(_ animated: Bool) {
   super.viewDidAppear(animated)
   UIApplication.shared.isIdleTimerDisabled = true
}
override func viewDidDisappear(_ animated: Bool) {
   super.viewDidDisappear(animated)
   UIApplication.shared.isIdleTimerDisabled = false
}
```

# 使用 IVS iOS 广播 SDK 发布和订阅 \$1 实时直播功能
<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)。

# iOS 如何选择相机分辨率和帧率
<a name="ios-publish-subscribe-resolution-framerate"></a>

由广播 SDK 管理的相机优化了其分辨率和帧率（每秒帧数或 FPS），以最大程度地减少发热和能耗。本节介绍如何选择分辨率和帧率以帮助主机应用程序针对其使用案例进行优化。

当使用 `IVSCamera` 创建 `IVSLocalStageStream` 时，会根据帧率 `IVSLocalStageStreamVideoConfiguration.targetFramerate` 和分辨率 `IVSLocalStageStreamVideoConfiguration.size` 优化相机。调用 `IVSLocalStageStream.setConfiguration` 会使用更新的值更新相机。

## 相机预览
<a name="resolution-framerate-camera-preview"></a>

如果您在不将 `IVSCamera` 连接到 `IVSBroadcastSession` 或 `IVSStage` 时创建其预览，则默认分辨率为 1080p，帧率为 60 fps。

## 广播舞台
<a name="resolution-framerate-broadcast-stage"></a>

使用 `IVSBroadcastSession` 广播 `IVSStage` 时，SDK 会尝试使用符合两个会话标准的分辨率和帧率来优化相机。

例如，如果广播配置的帧率设置为 15 FPS，分辨率设置为 1080p，而舞台的帧率为 30 FPS，分辨率为 720p，则 SDK 将选择帧率为 30 FPS、分辨率为 1080p 的相机配置。`IVSBroadcastSession` 会从相机中每隔一帧删除一帧，然后 `IVSStage` 将 1080p 的图像缩减到 720p。

如果主机应用程序计划将 `IVSBroadcastSession` 和 `IVSStage` 与相机结合使用，则建议相应配置的 `targetFramerate` 和 `size` 属性相匹配。不匹配可能会导致相机在捕获视频时自行重新配置，这将导致视频样本传输出现短暂延迟。

如果具有相同的值不符合主机应用程序的使用案例，则首先创建更高质量的相机将防止相机在添加较低质量会话时自行重新配置。例如，如果您以 1080p 和 30 FPS 进行广播，然后加入设为 720p 和 30 FPS 的舞台，则相机不会自行重新配置，视频将不间断地继续播放。这是因为 720p 小于或等于 1080p 而 30 FPS 小于或等于 30 FPS。

## 任意帧率、分辨率和宽高比
<a name="resolution-framerate-arbitrary"></a>

大多数相机硬件可以完全匹配常见格式，例如 30 FPS 时的 720p 或 60 FPS 时的 1080p。但是，您无法完全匹配所有格式。广播 SDK 根据以下规则（按优先顺序排列）选择相机配置：

1. 分辨率的宽度和高度大于或等于所需的分辨率，但是在此限制范围内，宽度和高度尽可能小。

1. 帧率大于或等于所需的帧率，但是在此限制范围内，帧率尽可能低。

1. 宽高比与所需的宽高比相匹配。

1. 如果有多种匹配格式，则使用视野最大的格式。

以下是两个示例：
+ 主机应用程序正在尝试以 120 FPS 的帧率按 4k 进行广播。所选相机在 60 FPS 时仅支持 4k 或在 120 FPS 时仅支持 1080p。所选格式在 60 FPS 时将为 4k，因为分辨率规则的优先级高于帧率规则。
+ 请求了一种不规则的分辨率，即 1910x1070。相机将使用 1920x1080。*注意：选择 1921x1080 之类的分辨率会导致相机纵向扩展到下一个可用分辨率（例如 2592x1944），这会导致 CPU 和内存带宽损失*。

## Android 的情况怎么样？
<a name="resolution-framerate-android"></a>

Android 不会像 iOS 那样即时调整其分辨率或帧率，因此这不会影响 Android 广播 SDK。

# IVS iOS 广播 SDK 中的已知问题和解决方法 \$1 实时直播功能
<a name="broadcast-ios-known-issues"></a>

本文档列出在使用 Amazon IVS 实时直播 iOS 广播 SDK 时可能遇到的已知问题，并提出可能的建议解决方法。
+ 更改蓝牙音频路由是不可预测的。如果您在会话中连接新设备，iOS 可能会自动更改输入路由，也可能不会。此外，无法在同一时间连接的多个蓝牙耳机之间进行选择。常规广播和舞台会话中均会发生此现象。

  **解决办法：**如果您打算使用蓝牙耳机，请在开始广播或进入舞台之前进行连接，并在整个会话期间保持连接状态。
+ 使用 iPhone 14、iPhone 14 Plus、iPhone 14 Pro 或 iPhone 14 Pro Max 的参与者可能会导致其他参与者出现音频回声问题。

  **解决方法：**使用受影响设备的参与者可以使用耳机来防止其他参与者出现回声问题。
+ 当一个参与者使用另一个参与者正在使用的令牌加入时，第一个连接将断开，不会出现具体错误提示。

  **解决办法**：尚无。
+ 有一个问题比较少见，即发布者正在发布，但订阅用户收到的发布状态是 `inactive`。

  **解决方法：**尝试退出然后加入会话。如果问题仍然存在，请为发布者创建新令牌。
+ 当参与者发布或订阅时，即使网络稳定，也可能会收到代码为 1400 的错误，表明由于网络问题而断开连接。

  **解决方法：**尝试重新发布/重新订阅。
+ 在舞台会话期间，通常在持续时间较长的通话中，可能会间歇性地出现罕见的音频失真问题。

  **解决方法：**遇到音频失真问题的参与者可以退出并重新加入会话，也可以取消发布并重新发布音频，以修复问题。

# IVS iOS 广播 SDK 中的错误处理 \$1 实时直播功能
<a name="broadcast-ios-error-handling"></a>

本节概述错误条件、IVS 实时直播 iOS 广播 SDK 如何向应用程序报告错误条件，以及在遇到这些错误时应用程序应采取的措施。

## 致命错误与非致命错误
<a name="broadcast-ios-fatal-vs-nonfatal-errors"></a>

错误对象带有“is fatal”布尔字段。这是 `IVSBroadcastErrorIsFatalKey` 下包含布尔值的字典条目。

通常，致命错误与阶段服务器的连接有关（连接无法建立，或者连接丢失且无法恢复）。应用程序应重新创建阶段并重新加入，可能使用新令牌或在设备连接恢复后重新加入。

非致命错误通常与发布/订阅状态有关，由 SDK 处理，SDK 会重试发布/订阅操作。

可以检查如下属性：

```
let nsError = error as NSError
if nsError.userInfo[IVSBroadcastErrorIsFatalKey] as? Bool == true {
  // the error is fatal
}
```

## 加入错误
<a name="broadcast-ios-stage-join-errors"></a>

### 令牌格式不正确
<a name="broadcast-ios-stage-join-errors-malformed-token"></a>

当阶段令牌格式不正确时，就会发生这种情况。

SDK 引发 Swift 异常，其错误代码 = 1000，IVSBroadcastErrorIsFatalKey = YES。

**操作**：创建有效令牌并重试加入。

### 令牌已过期
<a name="broadcast-ios-stage-join-errors-expired-token"></a>

当阶段令牌过期时，就会发生这种情况。

SDK 引发 Swift 异常，其错误代码 = 1001，IVSBroadcastErrorIsFatalKey = YES。

**操作**：创建新令牌并重试加入。

### 令牌无效或已撤销
<a name="broadcast-ios-stage-join-errors-invalid-token"></a>

当阶段令牌没有格式错误但被阶段服务器拒绝时，就会发生这种情况。通过应用程序提供的阶段渲染器异步报告此情况。

SDK 调用 `stage(didChange connectionState, withError error)` 时引发错误，其错误代码 = 1026，IVSBroadcastErrorIsFatalKey = YES。

**操作**：创建有效令牌并重试加入。

### 初始加入时出现网络错误
<a name="broadcast-ios-stage-join-errors-network-initial-join"></a>

当 SDK 无法联系阶段服务器建立连接时，就会发生这种情况。通过应用程序提供的阶段渲染器异步报告此情况。

SDK 调用 `stage(didChange connectionState, withError error)` 时引发错误，其错误代码 = 1300，IVSBroadcastErrorIsFatalKey = YES。

**操作**：等待设备连接恢复，然后重试加入。

### 已加入时出现网络错误
<a name="broadcast-ios-stage-join-errors-network-already-joined"></a>

如果设备的网络连接中断，SDK 可能会失去与阶段服务器的连接。通过应用程序提供的阶段渲染器异步报告此情况。

SDK 调用 `stage(didChange connectionState, withError error)` 时引发错误，其错误代码 = 1300，IVSBroadcastErrorIsFatalKey 值 = YES。

**操作**：等待设备连接恢复，然后重试加入。

## 发布/订阅错误
<a name="broadcast-ios-publish-subscribe-errors"></a>

### 初次
<a name="broadcast-ios-publish-subscribe-errors-initial"></a>

有如下几种错误：
+ MultihostSessionOfferCreationFailPublish（1020）
+ MultihostSessionOfferCreationFailSubscribe（1021）
+ MultihostSessionNoIceCandidates（1022）
+ MultihostSessionStageAtCapacity（1024）
+ SignallingSessionCannotRead（1201）
+ SignallingSessionCannotSend（1202）
+ SignallingSessionBadResponse（1203）

通过应用程序提供的阶段渲染器异步报告这些情况。

SDK 会在有限的次数内重试该操作。在重试期间，发布/订阅状态为 `ATTEMPTING_PUBLISH`/`ATTEMPTING_SUBSCRIBE`。如果重试成功，则状态将更改为 `PUBLISHED`/`SUBSCRIBED`。

SDK 调用 `IVSErrorDelegate:didEmitError` 时引发相关的错误代码，并且 `IVSBroadcastErrorIsFatalKey == NO`。

**操作**：无需执行任何操作，因为 SDK 会自动重试。或者，应用程序可以刷新策略以强制进行更多重试。

### 已经建立，然后失败
<a name="broadcast-ios-publish-subscribe-errors-established"></a>

发布或订阅在建立后可能会失败，很可能是由于网络错误所致。“对等连接由于未知的网络错误中断”的错误代码为 1400。

通过应用程序提供的阶段渲染器异步报告此情况。

SDK 会重试发布/订阅操作。在重试期间，发布/订阅状态为 `ATTEMPTING_PUBLISH`/`ATTEMPTING_SUBSCRIBE`。如果重试成功，则状态将更改为 `PUBLISHED`/`SUBSCRIBED`。

SDK 调用 `didEmitError` 时引发错误，其错误代码 = 1400，IVSBroadcastErrorIsFatalKey = NO。

**操作**：无需执行任何操作，因为 SDK 会自动重试。或者，应用程序可以刷新策略以强制进行更多重试。如果连接完全丢失，指向阶段的连接也可能失败。

# IVS 广播 SDK：混合设备
<a name="broadcast-mixed-devices"></a>

混合设备是音频和视频设备，采用多个输入源并生成单个输出。混合设备是一项强大的功能，可让您定义和管理多个屏幕上的（视频）元素和音轨。您可以组合来自多个来源的视频和音频，例如摄像头、麦克风、屏幕截图以及应用程序生成的音频和视频。您可以使用转换围绕流式传输到 IVS 的视频移动这些源，然后在流中添加和移除源。

混合设备分为图像和音频两种。要创建一个混合图像设备，请调用：

Android 上的 `DeviceDiscovery.createMixedImageDevice()`

iOS 上的 `IVSDeviceDiscovery.createMixedImageDevice()`

返回的设备可以附加到 `BroadcastSession`（低延迟流式传输）或 `Stage`（实时流式传输），就像任何其他设备一样。

## 术语
<a name="broadcast-mixed-devices-terminology"></a>

![\[IVS 广播混合设备术语。\]](http://docs.aws.amazon.com/zh_cn/ivs/latest/RealTimeUserGuide/images/Broadcast_SDK_Mixer_Glossary.png)



| 租期 | 说明 | 
| --- | --- | 
| 设备 | 一种硬件或软件组件，用于生成到音频或图像输入。例如，设备包括麦克风、摄像头、蓝牙耳机和屏幕截图或自定义图像输入等虚拟设备。 | 
| 混合设备 | 可以像其他任何 `Device` 一样附加到 `BroadcastSession` 的 `Device`，但带有允许添加 `Source` 对象的附加 API。混合设备具有内部混合器，其合成音频或图像，从而产生单个输出音频和图像流。 混合设备分为音频或图像两种。  | 
| 混合设备配置 | 混合设备的配置对象。对于混合图像设备，这将配置诸如尺寸和帧率之类的属性。对于混合音频设备，这将配置通道计数。 | 
|  来源 | 定义视觉元素在屏幕上的位置以及音频混合中音轨属性的容器。可以使用零个或多个源配置混合设备。向源提供了影响源媒体使用方式的配置。上图显示了四个图像源： [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/zh_cn/ivs/latest/RealTimeUserGuide/broadcast-mixed-devices.html)  | 
| 源配置 |  进入混合设备的源的配置对象。完整的配置对象如下所述。  | 
| Transition | 要将插槽移动到新位置或更改其部分属性，请使用 `MixedDevice.transitionToConfiguration()`。此方法需要： [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/zh_cn/ivs/latest/RealTimeUserGuide/broadcast-mixed-devices.html) | 

## 混合音频设备
<a name="broadcast-mixed-audio-device"></a>

### 配置
<a name="broadcast-mixed-audio-device-configuration"></a>

Android 上的 `MixedAudioDeviceConfiguration`

iOS 上的 `IVSMixedAudioDeviceConfiguration`


| 名称 | Type | 说明 | 
| --- | --- | --- | 
| `channels` | 整数 | 音频混合器的输出通道数。有效值：1、2。1 为单声道音频；2 为立体声音频。默认值：2。 | 

### 源配置
<a name="broadcast-mixed-audio-device-source-configuration"></a>

Android 上的 `MixedAudioDeviceSourceConfiguration`

iOS 上的 `IVSMixedAudioDeviceSourceConfiguration`


| 名称 | Type | 说明 | 
| --- | --- | --- | 
| `gain` | 浮点型 | 音频增益。这是一个乘数，因此任何高于 1 的值都会提高增益；任何低于 1 的值都会减小增益。有效值：0-2。默认值：1。 | 

## 混合图像设备
<a name="broadcast-mixed-image-device"></a>

### 配置
<a name="broadcast-mixed-image-device-configuration"></a>

Android 上的 `MixedImageDeviceConfiguration`

iOS 上的 `IVSMixedImageDeviceConfiguration`


| 名称 | Type | 说明 | 
| --- | --- | --- | 
| `size` | Vec2 | 视频画布的大小。 | 
| `targetFramerate` | 整数 | 混合设备的每秒目标帧数。通常情况下，该值应能够得到满足，但是在某些情况下（例如 CPU 或 GPU 负载过高），系统可能会丢弃帧。 | 
| `transparencyEnabled` | 布尔值 | 这允许使用图像源配置上的 `alpha` 属性进行混合。将其设置为 `true` 会增加内存和 CPU 消耗。默认值：`false`。 | 

### 源配置
<a name="broadcast-mixed-image-device-source-configuration"></a>

Android 上的 `MixedImageDeviceSourceConfiguration`

iOS 上的 `IVSMixedImageDeviceSourceConfiguration`


| 名称 | Type | 说明 | 
| --- | --- | --- | 
| `alpha` | 浮点型 | 插槽的 Alpha。这是与图像中的任何 Alpha 值相乘的结果。有效值：0-1。0 表示完全透明，1 表示完全不透明。默认值：1。 | 
| `aspect` | AspectMode | 适用于插槽中渲染的任何图像的宽高比模式。有效值： [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/zh_cn/ivs/latest/RealTimeUserGuide/broadcast-mixed-devices.html) 默认值：`Fit`  | 
| `fillColor` | Vec4 | 当插槽和图像的宽高比不匹配时，填充用于 `aspect Fit` 的颜色。格式为（red、green、blue、alpha）。有效值（对于每个通道）：0-1。默认值：(0, 0, 0, 0)。 | 
| `position` | Vec2 | 插槽相对于画布左上角的位置（以像素为单位）。插槽的原点也在左上角。 | 
| `size` | Vec2 | 插槽的大小（以像素为单位）。设置此值也会将 `matchCanvasSize` 设置为 `false`。默认值：(0, 0)；但是，因为 `matchCanvasSize` 默认为 `true`，插槽的渲染大小就是画布大小，而不是 (0, 0)。 | 
| `zIndex` | 浮点型 | 插槽的相对顺序。`zIndex` 值较高的插槽绘制在 `zIndex` 值较低的插槽之上。 | 

## 创建和配置混合图像设备
<a name="broadcast-mixed-image-device-creating-configuring"></a>

![\[配置广播会话以进行混合。\]](http://docs.aws.amazon.com/zh_cn/ivs/latest/RealTimeUserGuide/images/Broadcast_SDK_Mixer_Configuring.png)


在这里，我们创建了一个类似于本指南开头的场景，其中包含三个屏幕元素：
+ 左下角的摄像头插槽。
+ 右下角的徽标叠加插槽。
+ 右上角用于电影的插槽。

请注意，画布的原点是左上角，插槽也是一样。因此，将插槽定位在 (0, 0) 会将其放在左上角，且整个插槽可见。

### iOS
<a name="broadcast-mixed-image-device-creating-configuring-ios"></a>

```
let deviceDiscovery = IVSDeviceDiscovery()
let mixedImageConfig = IVSMixedImageDeviceConfiguration()
mixedImageConfig.size = CGSize(width: 1280, height: 720)
try mixedImageConfig.setTargetFramerate(60)
mixedImageConfig.isTransparencyEnabled = true
let mixedImageDevice = deviceDiscovery.createMixedImageDevice(with: mixedImageConfig)

// Bottom Left
let cameraConfig = IVSMixedImageDeviceSourceConfiguration()
cameraConfig.size = CGSize(width: 320, height: 180)
cameraConfig.position = CGPoint(x: 20, y: mixedImageConfig.size.height - cameraConfig.size.height - 20)
cameraConfig.zIndex = 2
let camera = deviceDiscovery.listLocalDevices().first(where: { $0 is IVSCamera }) as? IVSCamera
let cameraSource = IVSMixedImageDeviceSource(configuration: cameraConfig, device: camera)
mixedImageDevice.add(cameraSource)

// Top Right
let streamConfig = IVSMixedImageDeviceSourceConfiguration()
streamConfig.size = CGSize(width: 640, height: 320)
streamConfig.position = CGPoint(x: mixedImageConfig.size.width - streamConfig.size.width - 20, y: 20)
streamConfig.zIndex = 1
let streamDevice = deviceDiscovery.createImageSource(withName: "stream")
let streamSource = IVSMixedImageDeviceSource(configuration: streamConfig, device: streamDevice)
mixedImageDevice.add(streamSource)

// Bottom Right
let logoConfig = IVSMixedImageDeviceSourceConfiguration()
logoConfig.size = CGSize(width: 320, height: 180)
logoConfig.position = CGPoint(x: mixedImageConfig.size.width - logoConfig.size.width - 20,
                              y: mixedImageConfig.size.height - logoConfig.size.height - 20)
logoConfig.zIndex = 3
let logoDevice = deviceDiscovery.createImageSource(withName: "logo")
let logoSource = IVSMixedImageDeviceSource(configuration: logoConfig, device: logoDevice)
mixedImageDevice.add(logoSource)
```

### Android
<a name="broadcast-mixed-image-device-creating-configuring-android"></a>

```
val deviceDiscovery = DeviceDiscovery(this /* context */)
val mixedImageConfig = MixedImageDeviceConfiguration().apply {
    setSize(BroadcastConfiguration.Vec2(1280f, 720f))
    setTargetFramerate(60)
    setEnableTransparency(true)
}
val mixedImageDevice = deviceDiscovery.createMixedImageDevice(mixedImageConfig)

// Bottom Left
val cameraConfig = MixedImageDeviceSourceConfiguration().apply {
    setSize(BroadcastConfiguration.Vec2(320f, 180f))
    setPosition(BroadcastConfiguration.Vec2(20f, mixedImageConfig.size.y - size.y - 20))
    setZIndex(2)
}
val camera = deviceDiscovery.listLocalDevices().firstNotNullOf { it as? CameraSource }
val cameraSource = MixedImageDeviceSource(cameraConfig, camera)
mixedImageDevice.addSource(cameraSource)

// Top Right
val streamConfig = MixedImageDeviceSourceConfiguration().apply {
    setSize(BroadcastConfiguration.Vec2(640f, 320f))
    setPosition(BroadcastConfiguration.Vec2(mixedImageConfig.size.x - size.x - 20, 20f))
    setZIndex(1)
}
val streamDevice = deviceDiscovery.createImageInputSource(streamConfig.size)
val streamSource = MixedImageDeviceSource(streamConfig, streamDevice)
mixedImageDevice.addSource(streamSource)

// Bottom Right
val logoConfig = MixedImageDeviceSourceConfiguration().apply {
    setSize(BroadcastConfiguration.Vec2(320f, 180f))
    setPosition(BroadcastConfiguration.Vec2(mixedImageConfig.size.x - size.x - 20, mixedImageConfig.size.y - size.y - 20))
    setZIndex(1)
}
val logoDevice = deviceDiscovery.createImageInputSource(logoConfig.size)
val logoSource = MixedImageDeviceSource(logoConfig, logoDevice)
mixedImageDevice.addSource(logoSource)
```

## 移除源
<a name="broadcast-mixed-devices-removing-sources"></a>

要移除源，请使用要移除的 `Source` 对象调用 `MixedDevice.remove`。

## 动画转换
<a name="broadcast-mixed-devices-animations-transitions"></a>

转换方法用新配置替换源的配置。通过将持续时间设置为大于 0（以秒为单位），可以将此替换制作为随时间推移的动画。

### 哪些属性可以制作成动画？
<a name="broadcast-mixed-devices-animations-properties"></a>

插槽结构中并非所有属性都可以制作成动画。基于 Float 类型的任何属性都可以制作成动画；其他属性在动画的开始或结束时生效。


| 名称 | 可以制作成动画吗？ | 影响点 | 
| --- | --- | --- | 
| `Audio.gain` | 是 | 已插入 | 
| `Image.alpha` | 是 | 已插入 | 
| `Image.aspect` | 否 | 早于 | 
| `Image.fillColor` | 是 | 已插入 | 
| `Image.position` | 是 | 已插入 | 
| `Image.size` | 是 | 已插入 | 
| `Image.zIndex` 注意：`zIndex` 在 3D 空间中移动 2D 平面，因此当两个平面在动画中间的某个点交叉时，就会发生转换。这可以计算出来，但具体取决于开始和结束 `zIndex` 值。为了实现更顺畅的转换，请将其与 `alpha` 结合使用。  | 是 | 未知 | 

### 简单示例
<a name="broadcast-mixed-devices-animations-examples"></a>

以下是使用上述[创建和配置混合图像设备](#broadcast-mixed-image-device-creating-configuring)中定义的配置进行全屏摄像头接管的示例。将其在 0.5 秒以内的变化制作成动画。

#### iOS
<a name="broadcast-mixed-devices-animations-examples-ios"></a>

```
// Continuing the example from above, modifying the existing cameraConfig object.
cameraConfig.size = CGSize(width: 1280, height: 720)
cameraConfig.position = CGPoint.zero
cameraSource.transition(to: cameraConfig, duration: 0.5) { completed in
    if completed {
        print("Animation completed")
    } else {
        print("Animation interrupted")
    }
}
```

#### Android
<a name="broadcast-mixed-devices-animations-examples-android"></a>

```
// Continuing the example from above, modifying the existing cameraConfig object.
cameraConfig.setSize(BroadcastConfiguration.Vec2(1280f, 720f))
cameraConfig.setPosition(BroadcastConfiguration.Vec2(0f, 0f))
cameraSource.transitionToConfiguration(cameraConfig, 500) { completed ->
    if (completed) {
        print("Animation completed")
    } else {
        print("Animation interrupted")
    }
}
```

## 镜像广播
<a name="broadcast-mixed-devices-mirroring"></a>


| 要在此方向镜像广播中附加的映像设备... | 使用负值表示... | 
| --- | --- | 
| 水平 | 插槽的宽度 | 
| 垂直 | 插槽的高度 | 
| 水平和垂直方向 | 槽的宽度和高度 | 

需要按相同的值调整位置，以便在镜像时将槽置于正确的位置。

以下是水平和垂直镜像广播的示例。

### iOS
<a name="broadcast-mixed-devices-mirroring-ios"></a>

水平镜像：

```
let cameraSource = IVSMixedImageDeviceSourceConfiguration()
cameraSource.size = CGSize(width: -320, height: 720)
// Add 320 to position x since our width is -320
cameraSource.position = CGPoint(x: 320, y: 0)
```

垂直镜像：

```
let cameraSource = IVSMixedImageDeviceSourceConfiguration()
cameraSource.size = CGSize(width: 320, height: -720)
// Add 720 to position y since our height is -720
cameraSource.position = CGPoint(x: 0, y: 720)
```

### Android
<a name="broadcast-mixed-devices-mirroring-android"></a>

水平镜像：

```
val cameraConfig = MixedImageDeviceSourceConfiguration().apply {
    setSize(BroadcastConfiguration.Vec2(-320f, 180f))
   // Add 320f to position x since our width is -320f
    setPosition(BroadcastConfiguration.Vec2(320f, 0f))
}
```

垂直镜像：

```
val cameraConfig = MixedImageDeviceSourceConfiguration().apply {
    setSize(BroadcastConfiguration.Vec2(320f, -180f))
    // Add 180f to position y since our height is -180f
    setPosition(BroadcastConfiguration.Vec2(0f, 180f))
}
```

注意：此镜像与 `ImagePreviewView`（Android）和 `IVSImagePreviewView`（iOS）上的 `setMirrored` 方法不同。该方法仅影响设备上的本地预览视图，不会影响广播。

# IVS 广播 SDK：令牌交换 \$1 实时直播功能
<a name="broadcast-mobile-token-exchange"></a>

通过令牌交换，您可以升级或降级参与者令牌功能以及更新移动广播 SDK 中的令牌属性，而无需参与者重新连接。这对于联合主持等场景非常实用，即参与者可以首先使用仅限订阅的功能，以后再需要发布功能的场景。

限制：
+ 令牌交换仅支持在服务器上使用[密钥对](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/getting-started-distribute-tokens.html#getting-started-distribute-tokens-self-signed)创建的令牌，不支持通过 [CreateParticipantToken API](https://docs.aws.amazon.com/ivs/latest/RealTimeAPIReference/API_CreateParticipantToken.html) 创建的令牌。
+ 如果您使用令牌交换功能来更改驱动服务器端合成布局的属性（例如 featuredParticipantAttribute 和 participantOrderAttribute），则在参与者重新连接之前，活动合成的布局不会更新。

## 交换令牌
<a name="broadcast-mobile-token-exchange-exchanging-tokens"></a>

交换令牌的过程十分简单：在 `Stage`/`IVSStage` 对象上调用 `exchangeToken` API 并提供新令牌即可。如果新令牌的 `capabilities` 与原令牌不同，则会立即评估新令牌的功能。例如，假设原令牌没有 `publish` 功能而新令牌有，则会调用用于发布的舞台策略函数，从而让主机应用程序决定是要使用该新功能立即发布，还是等待。此过程也适用于已移除的功能：如果原令牌具有 `publish` 功能而新令牌没有，则参与者无需调用用于发布的舞台策略函数即可立即取消发布。

交换令牌时，原令牌和新令牌中以下有效载荷字段的值必须相同：
+ `topic`
+ `resource`
+ `jti`
+ `whip_url`
+ `events_url`

这些字段是不可变的。修改不可变字段的令牌交换会导致 SDK 立即拒绝该交换。

其余字段是可以更改的，包括：
+ `attributes`
+ `capabilities`
+ `user`
+ `_id`
+ `iat`
+ `exp`

### iOS
<a name="broadcast-mobile-token-exchange-exchanging-tokens-ios"></a>



```
let stage = try IVSStage(token: originalToken, strategy: self)
stage.join()
stage.exchangeToken(newToken)
```

### Android
<a name="broadcast-mobile-token-exchange-exchanging-tokens-android"></a>



```
val stage = Stage(context, originalToken, strategy)
stage.join()
stage.exchangeToken(newToken)
```

## 接收更新
<a name="broadcast-mobile-token-exchange-receiving-updates"></a>

`StageRenderer`/`IVSStageRenderer` 中的某个函数收到有关通过交换令牌来更新其 `userId` 或 `attributes` 的远程参与者已发布的更新。尚未发布的远程参与者如果最终发布，则将通过现有的 `onParticipantJoined`/`participantDidJoin` renderer 函数公开更新后的`userId` 和 `attributes`。

### iOS
<a name="broadcast-mobile-token-exchange-receiving-updates-ios"></a>



```
class MyStageRenderer: NSObject, IVSStageRenderer {
    func stage(_ stage: IVSStage, participantMetadataDidUpdate participant: IVSParticipantInfo) {
        // participant will be a new IVSParticipantInfo instance with updated properties.
    }
}
```

### Android
<a name="broadcast-mobile-token-exchange-receiving-updates-android"></a>



```
private val stageRenderer = object : StageRenderer {
    override fun onParticipantMetadataUpdated(stage: Stage, participantInfo: ParticipantInfo) {
        // participantInfo will be a new ParticipantInfo instance with updated properties.
    }
}
```

## 更新可见性
<a name="broadcast-mobile-token-exchange-visibility"></a>

当参与者通过交换令牌以更新其 `userId` 或 `attributes` 时，这些更改的可见性取决于其当前的发布状态：
+ **如果参与者*尚未*发布：**该更新将以静默方式处理。如果最终发布，则所有 SDK 都将通过初始发布事件收到更新后的 `userId` 和 `attributes`。
+ **如果参与者*已经*发布：**该更新将立即广播。但是，仅移动 SDKs v1.37.0\$1 会收到该通知。对于 Web SDK、早期版本的移动 SDK 和服务器端合成的参与者，在参与者取消发布并重新发布前不会看到该更改。

下表澄清了支持矩阵：


| 参与者状态 | 观察者：移动 SDK 1.37.0\$1 | 观察者：早期版本的移动 SDK、Web SDK、服务器端合成 | 
| --- | --- | --- | 
| 尚未发布（然后开始） | ✅ 可见（通过参与者加入的事件在发布时可见） | ✅ 可见（通过参与者加入的事件在发布时可见） | 
| 已发布（从未重新发布） | ✅ 可见（通过参与者元数据更新事件立即可见） | ❌ 不可见 | 
| 已发布（取消发布并重新发布） | ✅ 可见（通过参与者元数据更新事件立即可见） | ⚠️ 最终可见（通过参与者加入的事件在重新发布时可见） | 

# IVS 广播 SDK：自定义图像源 \$1 实时直播功能
<a name="broadcast-custom-image-sources"></a>

自定义图像输入源允许应用程序向广播 SDK 提供自己的图像输入，而不仅限于预设相机。自定义图像源可以是简单的半透明水印或静态“马上回来”等场景，也可以允许应用程序执行额外的自定义处理，例如向相机添加美颜滤镜。

当您使用自定义图像输入源对摄像机进行自定义控制时（例如使用需要访问相机的美颜滤镜库）时，广播 SDK 不再负责管理相机。相反，应用程序负责正确处理相机的生命周期。请参阅官方平台文档，以了解应用程序应如何管理相机。

## Android
<a name="custom-image-sources-android"></a>

创建 `DeviceDiscovery` 会话后，请创建一个图像输入源：

```
CustomImageSource imageSource = deviceDiscovery.createImageInputSource(new BroadcastConfiguration.Vec2(1280, 720));
```

此方法将返回一个 `CustomImageSource`，这是标准的 Android [Surface](https://developer.android.com/reference/android/view/Surface) 支持的图像源。子类 `SurfaceSource` 支持大小调整和轮换。您还可以创建 `ImagePreviewView` 来显示其内容预览。

检索底层 `Surface`：

```
Surface surface = surfaceSource.getInputSurface();
```

此 `Surface` 可以用作图像创建器（例如 Camera2、OpenGL ES 和其他库）的输出缓冲区。最简单的使用场景是直接在 Surface 画布中绘制静态位图或颜色。但是，许多库（例如 beauty-filter 库）提供了一种方法，允许应用程序指定外部 `Surface` 来进行渲染。您可以用这种方法将此 `Surface` 传递到滤镜库，从而让库输出处理后的帧以便广播会话进行流式传输。

此 `CustomImageSource` 可以包装在 `LocalStageStream` 中并由 `StageStrategy` 返回以发布到 `Stage`。

## iOS
<a name="custom-image-sources-ios"></a>

创建 `DeviceDiscovery` 会话后，请创建一个图像输入源：

```
let customSource = broadcastSession.createImageSource(withName: "customSourceName")
```

此方法将返回一个 `IVSCustomImageSource`，这是一个允许应用程序手动提交 `CMSampleBuffers` 的图像源。有关支持的像素格式，请参阅 iOS 广播 SDK 参考；指向最新版本的链接详见最新广播 SDK 发行版的 [Amazon IVS 版本注释](release-notes.md)。

提交到自定义源的样本将流式传输到舞台：

```
customSource.onSampleBuffer(sampleBuffer)
```

对于串流视频，请在回调中使用此方法。例如，假设您使用的是相机，则每次从 `AVCaptureSession` 收到一个新的样本缓冲时，应用程序可以将样本缓冲转发到自定义图像源。如果需要，应用程序可以在将样本提交到自定义图像源之前执行进一步的处理（例如美颜滤镜）。

`IVSCustomImageSource` 可以包装在 `IVSLocalStageStream` 中并由 `IVSStageStrategy` 返回以发布到 `Stage`。

# IVS 广播 SDK：自定义音频源 \$1 实时直播功能
<a name="broadcast-custom-audio-sources"></a>

**注意：**本指南仅适用于 IVS 实时直播功能 Android 广播 SDK。适用于 iOS 和 Web SDK 的信息将在未来发布。

通过使用自定义音频输入源，应用程序将可以向广播 SDK 提供自己的音频输入，而不仅限于设备的内置麦克风。通过使用自定义音频源，应用程序能够流式传输处理后带特效的音频、混合多个音频流或与第三方音频处理库集成。

使用自定义音频输入源时，广播 SDK 将不再负责直接管理麦克风。而由您的应用程序负责捕获、处理音频数据并将其提交到自定义源。

自定义音频源的工作流步骤如下：

1. 音频输入：创建具有指定音频格式（采样率、声道、格式）的自定义音频源。

1. 您进行的处理：从您的音频处理管道中捕获或生成音频数据。

1. 自定义音频源：使用 `appendBuffer()` 向该自定义源提交音频缓冲区。

1. 舞台：在 `LocalStageStream` 中打包并通过您的 `StageStrategy` 发布到舞台。

1. 参与者：舞台参与者实时接收处理后的音频。

## Android
<a name="custom-audio-sources-android"></a>

### 创建自定义音频源
<a name="custom-audio-sources-android-creating-a-custom-audio-source"></a>

创建 `DeviceDiscovery` 会话后，请创建一个自定义音频输入源：

```
DeviceDiscovery deviceDiscovery = new DeviceDiscovery(context); 
 
// Create custom audio source with specific format 
CustomAudioSource customAudioSource = deviceDiscovery.createAudioInputSource( 
   2,  // Number of channels (1 = mono, 2 = stereo) 
   BroadcastConfiguration.AudioSampleRate.RATE_48000,  // Sample rate 
   AudioDevice.Format.INT16  // Audio format (16-bit PCM) 
);
```

此方法会返回一个 `CustomAudioSource`，后者会接受原始 PCM 音频数据。自定义音频源配置的音频格式必须与音频处理管道生成的音频格式相同。

#### 支持的音频格式
<a name="custom-audio-sources-android-submitting-audio-data-supportedi-audio-formats"></a>


| 参数 | 选项 | 说明 | 
| --- | --- | --- | 
| 通道 | 1（单声道）、2（立体声）。 | 音频声道数。 | 
| 采样率 | RATE\$116000、RATE\$144100、RATE\$148000 | 音频采样率（以 Hz 为单位）。建议使用 48kHz 来获得高品质。 | 
| Format | INT16、FLOAT32 | 音频采样格式。INT16 是指 16 位定点 PCM，FLOAT32 是指 32 位浮点 PCM。同时支持交错格式和平面格式。 | 

### 提交音频数据
<a name="custom-audio-sources-android-submitting-audio-data"></a>

要将音频数据提交到自定义源，请使用 `appendBuffer()` 方法：

```
// Prepare audio data in a ByteBuffer 
ByteBuffer audioBuffer = ByteBuffer.allocateDirect(bufferSize); 
audioBuffer.put(pcmAudioData);  // Your processed audio data 
 
// Calculate the number of bytes 
long byteCount = pcmAudioData.length; 
 
// Submit audio to the custom source 
// presentationTimeUs should be generated by and come from your audio source
int samplesProcessed = customAudioSource.appendBuffer( 
   audioBuffer, 
   byteCount, 
   presentationTimeUs 
); 
 
if (samplesProcessed > 0) { 
   Log.d(TAG, "Successfully submitted " + samplesProcessed + " samples"); 
} else { 
   Log.w(TAG, "Failed to submit audio samples"); 
} 
 
// Clear buffer for reuse 
audioBuffer.clear();
```

**重要注意事项：**
+ 音频数据必须为创建自定义源时指定的格式。
+ 时间戳应单调增加，并由您的音频源提供，以确保流畅的音频播放。
+ 定期提交音频，以避免流式传输间断。
+ 该方法会返回已处理的样本数（0 表示失败）。

### 发布到舞台
<a name="custom-audio-sources-android-publishing-to-a-stage"></a>

将 `CustomAudioSource` 打包到某个 `AudioLocalStageStream` 中，然后它将从您的 `StageStrategy` 中返回：

```
// Create the audio stream from custom source 
AudioLocalStageStream audioStream = new AudioLocalStageStream(customAudioSource); 
 
// Define your stage strategy 
Strategy stageStrategy = new Strategy() { 
   @NonNull 
   @Override 
   public List<LocalStageStream> stageStreamsToPublishForParticipant( 
         @NonNull Stage stage, 
         @NonNull ParticipantInfo participantInfo) { 
      List<LocalStageStream> streams = new ArrayList<>(); 
      streams.add(audioStream);  // Publish custom audio 
      return streams; 
   } 
 
   @Override 
   public boolean shouldPublishFromParticipant( 
         @NonNull Stage stage, 
         @NonNull ParticipantInfo participantInfo) { 
      return true;  // Control when to publish 
   } 
 
   @Override 
   public Stage.SubscribeType shouldSubscribeToParticipant( 
         @NonNull Stage stage, 
         @NonNull ParticipantInfo participantInfo) { 
      return Stage.SubscribeType.AUDIO_VIDEO; 
   } 
}; 
 
// Create and join the stage 
Stage stage = new Stage(context, stageToken, stageStrategy);
```

### 完整示例：音频处理集成
<a name="custom-audio-sources-android-complete-example"></a>

以下展示了与一个音频处理 SDK 集成的完整示例：

```
public class AudioStreamingActivity extends AppCompatActivity { 
   private DeviceDiscovery deviceDiscovery; 
   private CustomAudioSource customAudioSource; 
   private AudioLocalStageStream audioStream; 
   private Stage stage; 
 
   @Override 
   protected void onCreate(Bundle savedInstanceState) { 
      super.onCreate(savedInstanceState); 
 
      // Configure audio manager 
      StageAudioManager.getInstance(this) 
         .setPreset(StageAudioManager.UseCasePreset.VIDEO_CHAT); 
 
      // Initialize IVS components 
      initializeIVSStage(); 
 
      // Initialize your audio processing SDK 
      initializeAudioProcessing(); 
   } 
 
   private void initializeIVSStage() { 
      deviceDiscovery = new DeviceDiscovery(this); 
 
      // Create custom audio source (48kHz stereo, 16-bit) 
      customAudioSource = deviceDiscovery.createAudioInputSource( 
         2,  // Stereo 
         BroadcastConfiguration.AudioSampleRate.RATE_48000, 
         AudioDevice.Format.INT16 
      ); 
 
      // Create audio stream 
      audioStream = new AudioLocalStageStream(customAudioSource); 
 
      // Create stage with strategy 
      Strategy strategy = new Strategy() { 
         @NonNull 
         @Override 
         public List<LocalStageStream> stageStreamsToPublishForParticipant( 
               @NonNull Stage stage, 
               @NonNull ParticipantInfo participantInfo) { 
            return Collections.singletonList(audioStream); 
         } 
 
         @Override 
         public boolean shouldPublishFromParticipant( 
               @NonNull Stage stage, 
               @NonNull ParticipantInfo participantInfo) { 
            return true; 
         } 
 
         @Override 
         public Stage.SubscribeType shouldSubscribeToParticipant( 
               @NonNull Stage stage, 
               @NonNull ParticipantInfo participantInfo) { 
            return Stage.SubscribeType.AUDIO_VIDEO; 
         } 
      }; 
 
      stage = new Stage(this, getStageToken(), strategy); 
   } 
 
   private void initializeAudioProcessing() { 
      // Initialize your audio processing SDK 
      // Set up callback to receive processed audio 
      yourAudioSDK.setAudioCallback(new AudioCallback() { 
         @Override 
         public void onProcessedAudio(byte[] audioData, int sampleRate, 
                                     int channels, long timestamp) { 
            // Submit processed audio to IVS Stage 
            submitAudioToStage(audioData, timestamp); 
         } 
      }); 
   } 
 
   // The timestamp is required to come from your audio source and you  
   // should not be generating one on your own, unless your audio source 
   // does not provide one. If that is the case, create your own epoch  
   // timestamp and manually calculate the duration between each sample  
   // using the number of frames and frame size. 

   private void submitAudioToStage(byte[] audioData, long timestamp) { 
      try { 
         // Allocate direct buffer 
         ByteBuffer buffer = ByteBuffer.allocateDirect(audioData.length); 
         buffer.put(audioData); 
 
         // Submit to custom audio source 
         int samplesProcessed = customAudioSource.appendBuffer( 
            buffer, 
            audioData.length, 
            timestamp > 0 ? timestamp : System.nanoTime() / 1000 
         ); 
 
         if (samplesProcessed <= 0) { 
            Log.w(TAG, "Failed to submit audio samples"); 
         } 
 
         buffer.clear(); 
      } catch (Exception e) { 
         Log.e(TAG, "Error submitting audio: " + e.getMessage(), e); 
      } 
   } 
 
   @Override 
   protected void onDestroy() { 
      super.onDestroy(); 
      if (stage != null) { 
          stage.release(); 
      } 
   } 
}
```

### 最佳实践
<a name="custom-audio-sources-android-best-practices"></a>

#### 音频格式一致性
<a name="custom-audio-sources-android-best-practices-audio-format-consistency"></a>

确保您提交的音频格式与创建自定义源时指定的格式相符：

```
// If you create with 48kHz stereo INT16 
customAudioSource = deviceDiscovery.createAudioInputSource( 
   2, RATE_48000, INT16 
); 
 
// Your audio data must be: 
// - 2 channels (stereo) 
// - 48000 Hz sample rate 
// - 16-bit interleaved PCM format
```

#### 缓冲区管理
<a name="custom-audio-sources-android-best-practices-buffer-managemetn"></a>

使用直接 `ByteBuffers` 并重复利用这些缓冲区，以尽可能减少垃圾回收：

```
// Allocate once 
private ByteBuffer audioBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE); 
 
// Reuse in callback 
public void onAudioData(byte[] data) { 
   audioBuffer.clear(); 
   audioBuffer.put(data); 
   customAudioSource.appendBuffer(audioBuffer, data.length, getTimestamp()); 
   audioBuffer.clear(); 
}
```

#### 定时与同步
<a name="custom-audio-sources-android-best-practices-timing-and-synchronization"></a>

您必须使用音频源提供的时间戳以确保流畅地播放音频。如果音频源未提供自己的时间戳，请自行创建纪元时间戳，并根据帧数和帧大小来手动计算采样间隔时间。

```
// "audioFrameTimestamp" should be generated by your audio source
// Consult your audio source’s documentation for information on how to get this 
long timestamp = audioFrameTimestamp;
```

#### 错误处理
<a name="custom-audio-sources-android-best-practices-error-handling"></a>

请务必检查 `appendBuffer()` 的返回值：

```
int samplesProcessed = customAudioSource.appendBuffer(buffer, count, timestamp); 
 
if (samplesProcessed <= 0) { 
   Log.w(TAG, "Audio submission failed - buffer may be full or format mismatch"); 
   // Handle error: check format, reduce submission rate, etc. 
}
```

# IVS 广播 SDK：第三方相机滤镜 \$1 实时直播功能
<a name="broadcast-3p-camera-filters"></a>

本指南假设您已经熟悉[自定义图像](broadcast-custom-image-sources.md)源以及将 [IVS 实时流式广播 SDK](broadcast.md) 集成到您的应用程序中。

相机滤镜使直播创作者能够增强或改变他们的面部或背景外观。这有可能提高观众的参与度、吸引观众并增强直播体验。

# 集成第三方相机滤镜
<a name="broadcast-3p-camera-filters-integrating"></a>

通过将滤镜 SDK 的输出馈送到[自定义图像输入源](broadcast-custom-image-sources.md)，您可以将第三方相机滤镜 SDK 与 IVS 广播 SDK 集成。自定义图像输入源允许应用程序向广播 SDK 提供自己的图像输入。第三方滤镜提供商的 SDK 可以管理相机的生命周期，以处理来自相机的图像、应用滤镜效果，并以可传递到自定义图像源的格式将其输出。

![\[通过将滤镜 SDK 的输出馈送到自定义图像输入源，将第三方相机滤镜 SDK 与 IVS 广播 SDK 集成。\]](http://docs.aws.amazon.com/zh_cn/ivs/latest/RealTimeUserGuide/images/3P_Camera_Filters_Integrating.png)


请参阅第三方滤镜提供者的文档，了解将应用了滤镜效果的相机帧转换为可以传递到[自定义图像输入](broadcast-custom-image-sources.md)源的格式的内置方法。该流程因所使用的 IVS 广播 SDK 版本而异：
+ **Web** — 滤镜提供者必须能够将其输出渲染到画布元素。然后，可以使用 [captureStream](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/captureStream) 方法返回画布内容的 MediaStream。然后，可以将 MediaStream 转换为 [LocalStageStream](https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-reference/classes/LocalStageStream) 的实例并发布到舞台。
+ **Android** — 滤镜提供者的 SDK 可以将帧渲染到 IVS 广播 SDK 提供的安卓 `Surface`，也可以将帧转换为位图。如果使用位图，则可以通过解锁并写入画布，将其渲染到自定义图像源提供的底层 `Surface`。
+ **iOS** — 第三方滤镜提供者的 SDK 必须提供应用了滤镜效果的相机帧作为 `CMSampleBuffer`。有关如何在处理相机图像之后获取 `CMSampleBuffer` 作为最终输出的信息，请参阅第三方滤镜提供者 SDK 的文档。

# 将 BytePlus 与 IVS 广播 SDK 结合使用
<a name="broadcast-3p-camera-filters-integrating-byteplus"></a>

本文档介绍如何将 BytePlus Effects SDK 与 IVS 广播 SDK 结合使用。

## Android
<a name="integrating-byteplus-android"></a>

### 安装和设置 BytePlus Effects SDK
<a name="integrating-byteplus-android-install-effects-sdk"></a>

有关如何安装、初始化和设置 BytePlus Effects SDK 的详细信息，请参阅 BytePlus [Android 访问指南](https://docs.byteplus.com/en/effects/docs/android-v4101-access-guide)。

### 设置自定义图像源
<a name="integrating-byteplus-android-setup-image-source"></a>

初始化 SDK 后，将经过处理并应用了滤镜效果的相机帧馈送到自定义图像输入源。为此，请创建 `DeviceDiscovery` 对象的实例并创建自定义图像源。请注意，当您使用自定义图像输入源对相机进行自定义控制时，广播 SDK 不再负责管理相机。相反，应用程序负责正确处理相机的生命周期。

#### Java
<a name="integrating-byteplus-android-setup-image-source-code"></a>

```
var deviceDiscovery = DeviceDiscovery(applicationContext)
var customSource = deviceDiscovery.createImageInputSource( BroadcastConfiguration.Vec2(
720F, 1280F
))
var surface: Surface = customSource.inputSurface
var filterStream = ImageLocalStageStream(customSource)
```

### 将输出转换为位图并馈送到自定义图像输入源
<a name="integrating-byteplus-android-convert-to-bitmap"></a>

为了使来自 BytePlus Effect SDK 的应用了滤镜效果的相机帧直接转发到 IVS 广播 SDK，请将纹理的 BytePlus Effects SDK 的输出转换为位图。处理图像时，SDK 会调用 `onDrawFrame()` 方法。`onDrawFrame()` 方法是 Android 的 [GLSurfaceView.Renderer](https://developer.android.com/reference/android/opengl/GLSurfaceView.Renderer) 界面中的一种公共方法。在 BytePlus 提供的 Android 示例应用程序中，在每个相机帧上都调用此方法；它输出纹理。同时，您可以使用逻辑来补充 `onDrawFrame()` 方法，将此纹理转换为位图并将其馈送到自定义图像输入源。如以下代码示例中所示，使用 BytePlus SDK 提供的 `transferTextureToBitmap` 方法进行此转换。此方法由来自 BytePlus Effects SDK 的 [com.bytedance.labcv.core.util.ImageUtil](https://docs.byteplus.com/en/effects/docs/android-v4101-access-guide#Appendix:%20convert%20input%20texture%20to%202D%20texture%20with%20upright%20face) 库提供，如以下代码示例中所示。然后，您可以将生成的位图写入 Surface 的画布以渲染到 `CustomImageSource` 的底层 Android `Surface`。多次连续调用 `onDrawFrame()` 会生成一系列位图，组合后会形成视频流。

#### Java
<a name="integrating-byteplus-android-convert-to-bitmap-code"></a>

```
import com.bytedance.labcv.core.util.ImageUtil;
...
protected ImageUtil imageUtility;
...


@Override
public void onDrawFrame(GL10 gl10) {
  ...	
  // Convert BytePlus output to a Bitmap
  Bitmap outputBt = imageUtility.transferTextureToBitmap(output.getTexture(),ByteEffect     
  Constants.TextureFormat.Texture2D,output.getWidth(), output.getHeight());

  canvas = surface.lockCanvas(null);
  canvas.drawBitmap(outputBt, 0f, 0f, null);
  surface.unlockCanvasAndPost(canvas);
```

# 将 DeepAR 与 IVS 广播 SDK 结合使用
<a name="broadcast-3p-camera-filters-integrating-deepar"></a>

本文档介绍如何将 DeepAR SDK 与 IVS 广播 SDK 结合使用。

## Android
<a name="integrating-deepar-android"></a>

有关如何将 DeepAR SDK 与 Android IVS 广播 SDK 集成的详细信息，请参阅 [Deepar 的 Android 集成指南](https://docs.deepar.ai/deepar-sdk/integrations/video-calling/amazon-ivs/android/)。

## iOS
<a name="integrating-deepar-ios"></a>

有关如何将 DeepAR SDK 与 iOS IVS 广播 SDK 集成的详细信息，请参阅 [Deepar 的 iOS 集成指南](https://docs.deepar.ai/deepar-sdk/integrations/video-calling/amazon-ivs/ios/)。

# 将 Snap 与 IVS 广播 SDK 结合使用
<a name="broadcast-3p-camera-filters-integrating-snap"></a>

本文档介绍如何将 Snap 的 Camera Kit SDK 与 IVS 广播 SDK 结合使用。

## Web
<a name="integrating-snap-web"></a>

本节假设您已经熟悉[使用 Web 广播 SDK 发布和订阅视频](getting-started-pub-sub-web.md)。

要集成 Snap 的 Camera Kit SDK 与 IVS 实时流式 Web 广播 SDK，您需要：

1. 安装 Camera Kit SDK 和 Webpack。（我们的示例使用 Webpack 作为捆绑程序，但您可以使用自己选择的任何捆绑程序。）

1. 创建 `index.html`。

1. 添加安装元素。

1. 创建`index.css`。

1. 显示和设置参与者。

1. 显示连接的相机和麦克风。

1. 创建 Camera Kit 会话。

1. 获取镜头并填充镜头选择器。

1. 将 Camera Kit 会话的输出渲染到画布。

1. 创建用于填充“镜头”下拉列表的函数。

1. 为 Camera Kit 提供用于渲染的媒体来源并发布 `LocalStageStream`。

1. 创建`package.json`。

1. 创建 Webpack 配置文件。

1. 设置 HTTPS 服务器并进行测试。

这些步骤中的各自的介绍如下。

### 安装 Camera Kit SDK 和 Webpack
<a name="integrating-snap-web-install-camera-kit"></a>

在此示例中，我们使用了 Webpack 作为捆绑程序；但是，您也可以使用任何捆绑程序。

```
npm i @snap/camera-kit webpack webpack-cli
```

### 创建 index.html
<a name="integrating-snap-web-create-index"></a>

接下来，创建 HTML 样板，并将 Web 广播 SDK 作为脚本标签导入。在下面的代码中，请务必将 `<SDK version>` 替换为您正在使用的广播 SDK 版本。

#### HTML
<a name="integrating-snap-web-create-index-code"></a>

```
<!--
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */
-->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <title>Amazon IVS Real-Time Streaming Web Sample (HTML and JavaScript)</title>

  <!-- Fonts and Styling -->
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css" />
  <link rel="stylesheet" href="./index.css" />

  <!-- Stages in Broadcast SDK -->
  <script src="https://web-broadcast.live-video.net/<SDK version>/amazon-ivs-web-broadcast.js"></script>
</head>

<body>
  <!-- Introduction -->
  <header>
    <h1>Amazon IVS Real-Time Streaming Web Sample (HTML and JavaScript)</h1>

    <p>This sample is used to demonstrate basic HTML / JS usage. <b><a href="https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/multiple-hosts.html">Use the AWS CLI</a></b> to create a <b>Stage</b> and a corresponding <b>ParticipantToken</b>. Multiple participants can load this page and put in their own tokens. You can <b><a href="https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#glossary" target="_blank">read more about stages in our public docs.</a></b></p>
  </header>
  <hr />
  
  <!-- Setup Controls -->
 
  <!-- Display Local Participants -->
  
  <!-- Lens Selector -->

  <!-- Display Remote Participants -->

  <!-- Load All Desired Scripts -->
```

### 添加安装元素
<a name="integrating-snap-web-add-setup-elements"></a>

创建 HTML，用于选择相机、麦克风和镜头，并指定参与者令牌：

#### HTML
<a name="integrating-snap-web-setup-controls-code"></a>

```
<!-- Setup Controls -->
  <div class="row">
    <div class="column">
      <label for="video-devices">Select Camera</label>
      <select disabled id="video-devices">
        <option selected disabled>Choose Option</option>
      </select>
    </div>
    <div class="column">
      <label for="audio-devices">Select Microphone</label>
      <select disabled id="audio-devices">
        <option selected disabled>Choose Option</option>
      </select>
    </div>
    <div class="column">
      <label for="token">Participant Token</label>
      <input type="text" id="token" name="token" />
    </div>
    <div class="column" style="display: flex; margin-top: 1.5rem">
      <button class="button" style="margin: auto; width: 100%" id="join-button">Join Stage</button>
    </div>
    <div class="column" style="display: flex; margin-top: 1.5rem">
      <button class="button" style="margin: auto; width: 100%" id="leave-button">Leave Stage</button>
    </div>
  </div>
```

在其下方添加其他 HTML 以显示来自本地和远程参与者的相机源：

#### HTML
<a name="integrating-snap-web-local-remote-participants-code"></a>

```
 <!-- Local Participant -->
<div class="row local-container">
    <canvas id="canvas"></canvas>

    <div class="column" id="local-media"></div>
    <div class="static-controls hidden" id="local-controls">
      <button class="button" id="mic-control">Mute Mic</button>
      <button class="button" id="camera-control">Mute Camera</button>
    </div>
  </div>

  
  <hr style="margin-top: 5rem"/>
  
  <!-- Remote Participants -->
  <div class="row">
    <div id="remote-media"></div>
  </div>
```

加载其他逻辑，包括用于设置相机的辅助方法和捆绑的 JavaScript 文件。（在本节的后面部分，您将创建这些 JavaScript 文件并将它们捆绑到单个文件中，这样您就可以将 Camera Kit 作为模块导入。捆绑的 JavaScript 文件将包含设置 Camera Kit、应用镜头以及将相机源及所应用镜头发布到舞台的逻辑。） 添加 `body` 和 `html` 元素的结束标签，以完成 `index.html` 的创建。

#### HTML
<a name="integrating-snap-web-load-all-scripts-code"></a>

```
<!-- Load all Desired Scripts -->
  <script src="./helpers.js"></script>
  <script src="./media-devices.js"></script>
  <!-- <script type="module" src="./stages-simple.js"></script> -->
  <script src="./dist/bundle.js"></script>
</body>
</html>
```

### 创建 index.css
<a name="integrating-snap-web-create-index-css"></a>

创建 CSS 源文件来设置页面样式。我们不会详细介绍这段代码，以便可以将重点放在用于管理暂存区和与 Snap 的 Camera Kit SDK 集成的逻辑上。

#### CSS
<a name="integrating-snap-web-create-index-css-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

html,
body {
  margin: 2rem;
  box-sizing: border-box;
  height: 100vh;
  max-height: 100vh;
  display: flex;
  flex-direction: column;
}

hr {
  margin: 1rem 0;
}

table {
  display: table;
}

canvas {
  margin-bottom: 1rem;
  background: green;
}

video {
  margin-bottom: 1rem;
  background: black;
  max-width: 100%;
  max-height: 150px;
}

.log {
  flex: none;
  height: 300px;
}

.content {
  flex: 1 0 auto;
}

.button {
  display: block;
  margin: 0 auto;
}

.local-container {
  position: relative;
}

.static-controls {
  position: absolute;
  margin-left: auto;
  margin-right: auto;
  left: 0;
  right: 0;
  bottom: -4rem;
  text-align: center;
}

.static-controls button {
  display: inline-block;
}

.hidden {
  display: none;
}

.participant-container {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  margin: 1rem;
}

video {
  border: 0.5rem solid #555;
  border-radius: 0.5rem;
}
.placeholder {
  background-color: #333333;
  display: flex;
  text-align: center;
  margin-bottom: 1rem;
}
.placeholder span {
  margin: auto;
  color: white;
}
#local-media {
  display: inline-block;
  width: 100vw;
}

#local-media video {
  max-height: 300px;
}

#remote-media {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: row;
  width: 100%;
}

#lens-selector {
  width: 100%;
  margin-bottom: 1rem;
}
```

### 显示和设置参与者
<a name="integrating-snap-web-setup-participants"></a>

接下来，创建 `helpers.js`，其中包含用于显示和设置参与者的辅助方法：

#### JavaScript
<a name="integrating-snap-web-setup-participants-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

function setupParticipant({ isLocal, id }) {
  const groupId = isLocal ? 'local-media' : 'remote-media';
  const groupContainer = document.getElementById(groupId);

  const participantContainerId = isLocal ? 'local' : id;
  const participantContainer = createContainer(participantContainerId);
  const videoEl = createVideoEl(participantContainerId);

  participantContainer.appendChild(videoEl);
  groupContainer.appendChild(participantContainer);

  return videoEl;
}

function teardownParticipant({ isLocal, id }) {
  const groupId = isLocal ? 'local-media' : 'remote-media';
  const groupContainer = document.getElementById(groupId);
  const participantContainerId = isLocal ? 'local' : id;

  const participantDiv = document.getElementById(
    participantContainerId + '-container'
  );
  if (!participantDiv) {
    return;
  }
  groupContainer.removeChild(participantDiv);
}

function createVideoEl(id) {
  const videoEl = document.createElement('video');
  videoEl.id = id;
  videoEl.autoplay = true;
  videoEl.playsInline = true;
  videoEl.srcObject = new MediaStream();
  return videoEl;
}

function createContainer(id) {
  const participantContainer = document.createElement('div');
  participantContainer.classList = 'participant-container';
  participantContainer.id = id + '-container';

  return participantContainer;
}
```

### 显示已连接的相机和麦克风
<a name="integrating-snap-web-display-cameras-microphones"></a>

接下来，创建 `media-devices.js`，其中包含用于显示连接到设备的相机和麦克风的辅助方法：

#### JavaScript
<a name="integrating-snap-web-display-cameras-microphones-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

/**
 * Returns an initial list of devices populated on the page selects
 */
async function initializeDeviceSelect() {
  const videoSelectEl = document.getElementById('video-devices');
  videoSelectEl.disabled = false;

  const { videoDevices, audioDevices } = await getDevices();
  videoDevices.forEach((device, index) => {
    videoSelectEl.options[index] = new Option(device.label, device.deviceId);
  });

  const audioSelectEl = document.getElementById('audio-devices');

  audioSelectEl.disabled = false;
  audioDevices.forEach((device, index) => {
    audioSelectEl.options[index] = new Option(device.label, device.deviceId);
  });
}

/**
 * Returns all devices available on the current device
 */
async function getDevices() {
  // Prevents issues on Safari/FF so devices are not blank
  await navigator.mediaDevices.getUserMedia({ video: true, audio: true });

  const devices = await navigator.mediaDevices.enumerateDevices();
  // Get all video devices
  const videoDevices = devices.filter((d) => d.kind === 'videoinput');
  if (!videoDevices.length) {
    console.error('No video devices found.');
  }

  // Get all audio devices
  const audioDevices = devices.filter((d) => d.kind === 'audioinput');
  if (!audioDevices.length) {
    console.error('No audio devices found.');
  }

  return { videoDevices, audioDevices };
}

async function getCamera(deviceId) {
  // Use Max Width and Height
  return navigator.mediaDevices.getUserMedia({
    video: {
      deviceId: deviceId ? { exact: deviceId } : null,
    },
    audio: false,
  });
}

async function getMic(deviceId) {
  return navigator.mediaDevices.getUserMedia({
    video: false,
    audio: {
      deviceId: deviceId ? { exact: deviceId } : null,
    },
  });
}
```

### 创建 Camera Kit 会话
<a name="integrating-snap-web-camera-kit-session"></a>

创建 `stages.js`，其中包含将镜头应用于相机视频源并将该视频源发布到舞台的逻辑。我们建议将以下代码块复制并粘贴到 `stages.js`。然后，您可以逐段查看代码，以了解以下各节中将会发生什么。

#### JavaScript
<a name="integrating-snap-web-camera-kit-session-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

const {
  Stage,
  LocalStageStream,
  SubscribeType,
  StageEvents,
  ConnectionState,
  StreamType,
} = IVSBroadcastClient;

import {
  bootstrapCameraKit,
  createMediaStreamSource,
  Transform2D,
} from '@snap/camera-kit';

let cameraButton = document.getElementById('camera-control');
let micButton = document.getElementById('mic-control');
let joinButton = document.getElementById('join-button');
let leaveButton = document.getElementById('leave-button');

let controls = document.getElementById('local-controls');
let videoDevicesList = document.getElementById('video-devices');
let audioDevicesList = document.getElementById('audio-devices');

let lensSelector = document.getElementById('lens-selector');
let session;
let availableLenses = [];

// Stage management
let stage;
let joining = false;
let connected = false;
let localCamera;
let localMic;
let cameraStageStream;
let micStageStream;

const liveRenderTarget = document.getElementById('canvas');

const init = async () => {
  await initializeDeviceSelect();

  const cameraKit = await bootstrapCameraKit({
    apiToken: 'INSERT_YOUR_API_TOKEN_HERE',
  });

  session = await cameraKit.createSession({ liveRenderTarget });
  const { lenses } = await cameraKit.lensRepository.loadLensGroups([
    'INSERT_YOUR_LENS_GROUP_ID_HERE',
  ]);

  availableLenses = lenses;
  populateLensSelector(lenses);

  const snapStream = liveRenderTarget.captureStream();

  lensSelector.addEventListener('change', handleLensChange);
  lensSelector.disabled = true;
  cameraButton.addEventListener('click', () => {
    const isMuted = !cameraStageStream.isMuted;
    cameraStageStream.setMuted(isMuted);
    cameraButton.innerText = isMuted ? 'Show Camera' : 'Hide Camera';
  });

  micButton.addEventListener('click', () => {
    const isMuted = !micStageStream.isMuted;
    micStageStream.setMuted(isMuted);
    micButton.innerText = isMuted ? 'Unmute Mic' : 'Mute Mic';
  });

  joinButton.addEventListener('click', () => {
    joinStage(session, snapStream);
  });

  leaveButton.addEventListener('click', () => {
    leaveStage();
  });
};

async function setCameraKitSource(session, mediaStream) {
  const source = createMediaStreamSource(mediaStream);
  await session.setSource(source);
  source.setTransform(Transform2D.MirrorX);
  session.play();
}

const populateLensSelector = (lenses) => {
  lensSelector.innerHTML = '<option selected disabled>Choose Lens</option>';

  lenses.forEach((lens, index) => {
    const option = document.createElement('option');
    option.value = index;
    option.text = lens.name || `Lens ${index + 1}`;
    lensSelector.appendChild(option);
  });
};

const handleLensChange = (event) => {
  const selectedIndex = parseInt(event.target.value);
  if (session && availableLenses[selectedIndex]) {
    session.applyLens(availableLenses[selectedIndex]);
  }
};

const joinStage = async (session, snapStream) => {
  if (connected || joining) {
    return;
  }
  joining = true;

  const token = document.getElementById('token').value;

  if (!token) {
    window.alert('Please enter a participant token');
    joining = false;
    return;
  }

  // Retrieve the User Media currently set on the page
  localCamera = await getCamera(videoDevicesList.value);
  localMic = await getMic(audioDevicesList.value);
  await setCameraKitSource(session, localCamera);

  // Create StageStreams for Audio and Video
  cameraStageStream = new LocalStageStream(snapStream.getVideoTracks()[0]);
  micStageStream = new LocalStageStream(localMic.getAudioTracks()[0]);

  const strategy = {
    stageStreamsToPublish() {
      return [cameraStageStream, micStageStream];
    },
    shouldPublishParticipant() {
      return true;
    },
    shouldSubscribeToParticipant() {
      return SubscribeType.AUDIO_VIDEO;
    },
  };

  stage = new Stage(token, strategy);

  // Other available events:
  // https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#events
  stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {
    connected = state === ConnectionState.CONNECTED;

    if (connected) {
      joining = false;
      controls.classList.remove('hidden');
      lensSelector.disabled = false;
    } else {
      controls.classList.add('hidden');
      lensSelector.disabled = true;
    }
  });

  stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {
    console.log('Participant Joined:', participant);
  });

  stage.on(
    StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED,
    (participant, streams) => {
      console.log('Participant Media Added: ', participant, streams);

      let streamsToDisplay = streams;

      if (participant.isLocal) {
        // Ensure to exclude local audio streams, otherwise echo will occur
        streamsToDisplay = streams.filter(
          (stream) => stream.streamType === StreamType.VIDEO
        );
      }

      const videoEl = setupParticipant(participant);
      streamsToDisplay.forEach((stream) =>
        videoEl.srcObject.addTrack(stream.mediaStreamTrack)
      );
    }
  );

  stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant) => {
    console.log('Participant Left: ', participant);
    teardownParticipant(participant);
  });

  try {
    await stage.join();
  } catch (err) {
    joining = false;
    connected = false;
    console.error(err.message);
  }
};

const leaveStage = async () => {
  stage.leave();

  joining = false;
  connected = false;

  cameraButton.innerText = 'Hide Camera';
  micButton.innerText = 'Mute Mic';
  controls.classList.add('hidden');
};

init();
```

在本文件的第一部分，我们导入广播 SDK 和 Camera Kit Web SDK，并初始化将用于每个 SDK 的变量。我们通过在[启动 Camera Kit Web SDK](https://kit.snapchat.com/reference/CameraKit/web/0.7.0/index.html#bootstrapping-the-sdk) 后调用 `createSession` 来创建 Camera Kit 会话。请注意，画布元素对象会传递到会话；这会告诉 Camera Kit 渲染到该画布中。

#### JavaScript
<a name="integrating-snap-web-camera-kit-session-code-2"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

const {
  Stage,
  LocalStageStream,
  SubscribeType,
  StageEvents,
  ConnectionState,
  StreamType,
} = IVSBroadcastClient;

import {
  bootstrapCameraKit,
  createMediaStreamSource,
  Transform2D,
} from '@snap/camera-kit';

let cameraButton = document.getElementById('camera-control');
let micButton = document.getElementById('mic-control');
let joinButton = document.getElementById('join-button');
let leaveButton = document.getElementById('leave-button');

let controls = document.getElementById('local-controls');
let videoDevicesList = document.getElementById('video-devices');
let audioDevicesList = document.getElementById('audio-devices');

let lensSelector = document.getElementById('lens-selector');
let session;
let availableLenses = [];

// Stage management
let stage;
let joining = false;
let connected = false;
let localCamera;
let localMic;
let cameraStageStream;
let micStageStream;

const liveRenderTarget = document.getElementById('canvas');

const init = async () => {
  await initializeDeviceSelect();

  const cameraKit = await bootstrapCameraKit({
    apiToken: 'INSERT_YOUR_API_TOKEN_HERE',
  });

  session = await cameraKit.createSession({ liveRenderTarget });
```

### 获取镜头并填充镜头选择器
<a name="integrating-snap-web-fetch-apply-lens"></a>

要获取镜头，请将镜头组 ID 占位符替换为自己的占位符，可在 [Camera Kit 开发人员门户](https://camera-kit.snapchat.com/)中找到此占位符。使用我们稍后将创建的 `populateLensSelector()` 函数填充“镜头”选择下拉列表。

#### JavaScript
<a name="integrating-snap-web-fetch-apply-lens-code"></a>

```
session = await cameraKit.createSession({ liveRenderTarget });
  const { lenses } = await cameraKit.lensRepository.loadLensGroups([
    'INSERT_YOUR_LENS_GROUP_ID_HERE',
  ]);

  availableLenses = lenses;
  populateLensSelector(lenses);
```

### 将 Camera Kit 会话的输出渲染到画布
<a name="integrating-snap-web-render-output-to-canvas"></a>

使用 [captureStream](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/captureStream) 方法返回画布内容的 `MediaStream`。画布将包含已应用镜头的相机视频源的视频流。此外，为用于将相机和麦克风静音的按钮添加事件侦听器，以及用于加入和离开舞台的事件侦听器。在加入舞台的事件侦听器中，我们传入 Camera Kit 会话以及来自画布的 `MediaStream`，这样它就可以发布到舞台。

#### JavaScript
<a name="integrating-snap-web-render-output-to-canvas-code"></a>

```
const snapStream = liveRenderTarget.captureStream();

  lensSelector.addEventListener('change', handleLensChange);
  lensSelector.disabled = true;
  cameraButton.addEventListener('click', () => {
    const isMuted = !cameraStageStream.isMuted;
    cameraStageStream.setMuted(isMuted);
    cameraButton.innerText = isMuted ? 'Show Camera' : 'Hide Camera';
  });

  micButton.addEventListener('click', () => {
    const isMuted = !micStageStream.isMuted;
    micStageStream.setMuted(isMuted);
    micButton.innerText = isMuted ? 'Unmute Mic' : 'Mute Mic';
  });

  joinButton.addEventListener('click', () => {
    joinStage(session, snapStream);
  });

  leaveButton.addEventListener('click', () => {
    leaveStage();
  });
};
```

### 创建用于填充“镜头”下拉列表的函数
<a name="integrating-snap-web-populate-lens-dropdown"></a>

创建以下函数，用之前获取的镜头来填充**镜头**选择器。**镜头**选择器是页面上的一个 UI 元素，可让您从镜头列表中选择要应用于相机源的镜头。此外，请创建 `handleLensChange` 回调函数，以便从**镜头**下拉列表中选择指定镜头时应用该镜头。

#### JavaScript
<a name="integrating-snap-web-populate-lens-dropdown-code"></a>

```
const populateLensSelector = (lenses) => {
  lensSelector.innerHTML = '<option selected disabled>Choose Lens</option>';

  lenses.forEach((lens, index) => {
    const option = document.createElement('option');
    option.value = index;
    option.text = lens.name || `Lens ${index + 1}`;
    lensSelector.appendChild(option);
  });
};

const handleLensChange = (event) => {
  const selectedIndex = parseInt(event.target.value);
  if (session && availableLenses[selectedIndex]) {
    session.applyLens(availableLenses[selectedIndex]);
  }
};
```

### 为 Camera Kit 提供用于渲染的媒体来源并发布 LocalStageStream
<a name="integrating-snap-web-publish-localstagestream"></a>

要发布应用了镜头的视频流，请创建一个调用 `setCameraKitSource` 的函数，用于传入之前从画布上捕获的 `MediaStream`。画布中的 `MediaStream` 目前没有执行任何操作，因为我们还没有整合本地相机源。我们可以通过调用 `getCamera` 辅助方法并将其分配给 `localCamera` 来整合本地相机源。然后，我们可以将本地相机源（通过 `localCamera`）和会话对象传递给 `setCameraKitSource`。`setCameraKitSource` 函数通过调用 `createMediaStreamSource` 将我们的本地相机源转换为 [CameraKit 的媒体源](https://docs.snap.com/camera-kit/integrate-sdk/web/web-configuration#creating-a-camerakitsource)。然后，[转换](https://docs.snap.com/camera-kit/integrate-sdk/web/web-configuration#2d-transforms) `CameraKit` 的媒体源以生成前置相头的镜像。然后，镜头效果应用于媒体源，并通过调用 `session.play()` 将其渲染到输出画布。

现在，将镜头应用于从画布捕获的 `MediaStream` 后，我们可以继续将其发布到舞台。为此，我们使用来自 `MediaStream` 的视频轨道创建 `LocalStageStream`。然后，可以将 `LocalStageStream` 的实例传递到要发布的 `StageStrategy`。

#### JavaScript
<a name="integrating-snap-web-publish-localstagestream-code"></a>

```
async function setCameraKitSource(session, mediaStream) {
  const source = createMediaStreamSource(mediaStream);
  await session.setSource(source);
  source.setTransform(Transform2D.MirrorX);
  session.play();
}

const joinStage = async (session, snapStream) => {
  if (connected || joining) {
    return;
  }
  joining = true;

  const token = document.getElementById('token').value;

  if (!token) {
    window.alert('Please enter a participant token');
    joining = false;
    return;
  }

  // Retrieve the User Media currently set on the page
  localCamera = await getCamera(videoDevicesList.value);
  localMic = await getMic(audioDevicesList.value);
  await setCameraKitSource(session, localCamera);
  // Create StageStreams for Audio and Video
  // cameraStageStream = new LocalStageStream(localCamera.getVideoTracks()[0]);
  cameraStageStream = new LocalStageStream(snapStream.getVideoTracks()[0]);
  micStageStream = new LocalStageStream(localMic.getAudioTracks()[0]);

  const strategy = {
    stageStreamsToPublish() {
      return [cameraStageStream, micStageStream];
    },
    shouldPublishParticipant() {
      return true;
    },
    shouldSubscribeToParticipant() {
      return SubscribeType.AUDIO_VIDEO;
    },
  };
```

下面的其余代码用于创建和管理我们的舞台：

#### JavaScript
<a name="integrating-snap-web-create-manage-stage-code"></a>

```
stage = new Stage(token, strategy);

  // Other available events:
  // https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#events

  stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {
    connected = state === ConnectionState.CONNECTED;

    if (connected) {
      joining = false;
      controls.classList.remove('hidden');
    } else {
      controls.classList.add('hidden');
    }
  });

  stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {
    console.log('Participant Joined:', participant);
  });

  stage.on(
    StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED,
    (participant, streams) => {
      console.log('Participant Media Added: ', participant, streams);

      let streamsToDisplay = streams;

      if (participant.isLocal) {
        // Ensure to exclude local audio streams, otherwise echo will occur
        streamsToDisplay = streams.filter(
          (stream) => stream.streamType === StreamType.VIDEO
        );
      }

      const videoEl = setupParticipant(participant);
      streamsToDisplay.forEach((stream) =>
        videoEl.srcObject.addTrack(stream.mediaStreamTrack)
      );
    }
  );

  stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant) => {
    console.log('Participant Left: ', participant);
    teardownParticipant(participant);
  });

  try {
    await stage.join();
  } catch (err) {
    joining = false;
    connected = false;
    console.error(err.message);
  }
};

const leaveStage = async () => {
  stage.leave();

  joining = false;
  connected = false;

  cameraButton.innerText = 'Hide Camera';
  micButton.innerText = 'Mute Mic';
  controls.classList.add('hidden');
};

init();
```

### 创建 package.json
<a name="integrating-snap-web-package-json"></a>

创建 `package.json` 并添加以下 JSON 配置。该文件定义了我们的依赖项，并包含用于捆绑代码的脚本命令。

#### JSON 配置
<a name="integrating-snap-web-package-json-code"></a>

```
{
  "dependencies": {
    "@snap/camera-kit": "^0.10.0"
  },
  "name": "ivs-stages-with-snap-camerakit",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "webpack": "^5.95.0",
    "webpack-cli": "^5.1.4"
  }
}
```

### 创建 Webpack 配置文件
<a name="integrating-snap-web-webpack-config"></a>

创建 `webpack.config.js` 并添加以下代码。这捆绑了我们到目前为止所创建的代码，以便我们能够利用 import 语句来使用 Camera Kit。

#### JavaScript
<a name="integrating-snap-web-webpack-config-code"></a>

```
const path = require('path');
module.exports = {
  entry: ['./stage.js'],
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};
```

最后，运行 `npm run build` 以按照 Webpack 配置文件中的定义捆绑您的 JavaScript。出于测试目的，您随后可以从本地计算机提供 HTML 和 JavaScript。在此示例中，我们使用了 Python 的 `http.server` 模块。

### 设置 HTTPS 服务器并进行测试
<a name="integrating-snap-web-https-server-test"></a>

要测试代码，我们需要设置 HTTPS 服务器。使用 HTTPS 服务器进行本地开发并测试 Web 应用程序与 Snap Camera Kit SDK 的集成将有助于避免 CORS（跨源资源共享）问题。

打开终端并导航到创建了目前为止所有代码的目录。运行以下命令以生成自签名 SSL/TLS 证书和私有密钥：

```
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
```

将创建两个文件：`key.pem`（私有密钥）和 `cert.pem`（自签名证书）。新建一个名为 `https_server.py` 的 Python 文件，并添加以下代码：

#### Python
<a name="integrating-snap-web-https-server-test-code"></a>

```
import http.server
import ssl

# Set the directory to serve files from
DIRECTORY = '.'

# Create the HTTPS server
server_address = ('', 4443)
httpd = http.server.HTTPServer(
    server_address, http.server.SimpleHTTPRequestHandler)

# Wrap the socket with SSL/TLS
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('cert.pem', 'key.pem')
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)

print(f'Starting HTTPS server on https://localhost:4443, serving {DIRECTORY}')
httpd.serve_forever()
```

打开终端，导航到创建 `https_server.py` 文件的目录，然后运行以下命令：

```
python3 https_server.py
```

这将启动 https://localhost:4443 上的 HTTPS 服务器，提供来自当前目录的文件。确保 `cert.pem` 和 `key.pem` 文件与 `https_server.py` 文件位于同一目录中。

打开浏览器并导航至 https://localhost:4443。由于这是一个自签名 SSL/TLS 证书，您的 Web 浏览器不会信任它，所以您将会收到警告。由于这仅用于测试目的，您可以绕过该警告。然后，您应该会在屏幕上看到之前指定的 Snap Lens 的 AR 效果已应用到相机视频源。

请注意，这种使用 Python 内置 `http.server` 和 `ssl` 模块的设置适用于本地开发和测试目的，但不建议将其用于生产环境。Web 浏览器和其他客户端不信任此设置中使用的自签名 SSL/TLS 证书，这意味着用户在访问服务器时会遇到安全警告。此外，尽管我们在此示例中使用了 Python 的内置 http.server 和 ssl 模块，但您仍可以选择使用其他 HTTPS 服务器解决方案。

## Android
<a name="integrating-snap-android"></a>

要将 Snap 的 Camera Kit SDK 与 IVS Android 广播 SDK 集成，您必须安装 Camera Kit SDK，初始化 Camera Kit 会话，应用镜头并将 Camera Kit 会话的输出馈送到自定义图像输入源。

要安装 Camera Kit SDK，请将以下内容添加到您的模块的 `build.gradle` 文件中。将 `$cameraKitVersion` 替换为[最新的 Camera Kit SDK 版本](https://docs.snap.com/camera-kit/integrate-sdk/mobile/changelog-mobile)。

### Java
<a name="integrating-snap-android-install-camerakit-sdk-code"></a>

```
implementation "com.snap.camerakit:camerakit:$cameraKitVersion"
```

初始化并获取 `cameraKitSession`。Camera Kit 还为 Android 的 [CameraX](https://developer.android.com/media/camera/camerax) API 提供了便捷的包装器，因此您无需编写复杂的逻辑即可将 CameraX 与 Camera Kit 一起使用。您可以将 `CameraXImageProcessorSource` 对象用作 [ImageProcessor](https://snapchat.github.io/camera-kit-reference/api/android/latest/-camera-kit/com.snap.camerakit/-image-processor/index.html) 的[来源](https://snapchat.github.io/camera-kit-reference/api/android/latest/-camera-kit/com.snap.camerakit/-source/index.html)，这样您就可以开始相机预览流式帧了。

### Java
<a name="integrating-snap-android-initialize-camerakitsession-code"></a>

```
 protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        // Camera Kit support implementation of ImageProcessor that is backed by CameraX library:
        // https://developer.android.com/training/camerax
        CameraXImageProcessorSource imageProcessorSource = new CameraXImageProcessorSource( 
            this /*context*/, this /*lifecycleOwner*/
        );
        imageProcessorSource.startPreview(true /*cameraFacingFront*/);

        cameraKitSession = Sessions.newBuilder(this)
                .imageProcessorSource(imageProcessorSource)
                .attachTo(findViewById(R.id.camerakit_stub))
                .build();
    }
```

### 获取并应用镜头
<a name="integrating-snap-android-fetch-apply-lenses"></a>

您可以在 [Camera Kit 开发人员门户](https://camera-kit.snapchat.com/)的轮盘中配置镜头及其顺序：

#### Java
<a name="integrating-snap-android-configure-lenses-code"></a>

```
// Fetch lenses from repository and apply them
 // Replace LENS_GROUP_ID with Lens Group ID from https://camera-kit.snapchat.com
cameraKitSession.getLenses().getRepository().get(new Available(LENS_GROUP_ID), available -> {
     Log.d(TAG, "Available lenses: " + available);
     Lenses.whenHasFirst(available, lens -> cameraKitSession.getLenses().getProcessor().apply(lens, result -> {
          Log.d(TAG,  "Apply lens [" + lens + "] success: " + result);
      }));
});
```

要进行广播，请将处理后的帧发送到自定义图像源的底层 `Surface`。使用 `DeviceDiscovery` 对象并创建 `CustomImageSource` 以返回 `SurfaceSource`。然后，您可以将 `CameraKit` 会话的输出渲染到 `SurfaceSource` 提供的底层 `Surface`。

#### Java
<a name="integrating-snap-android-broadcast-code"></a>

```
val publishStreams = ArrayList<LocalStageStream>()

val deviceDiscovery = DeviceDiscovery(applicationContext)
val customSource = deviceDiscovery.createImageInputSource(BroadcastConfiguration.Vec2(720f, 1280f))

cameraKitSession.processor.connectOutput(outputFrom(customSource.inputSurface))
val customStream = ImageLocalStageStream(customSource)

// After rendering the output from a Camera Kit session to the Surface, you can 
// then return it as a LocalStageStream to be published by the Broadcast SDK
val customStream: ImageLocalStageStream = ImageLocalStageStream(surfaceSource)
publishStreams.add(customStream)

@Override
fun stageStreamsToPublishForParticipant(stage: Stage, participantInfo: ParticipantInfo): List<LocalStageStream> = publishStreams
```

# 将背景替换功能与 IVS 广播 SDK 结合使用
<a name="broadcast-3p-camera-filters-background-replacement"></a>

背景替换是一种相机滤镜，它使直播创作者能够更改其背景。如下图所示，替换背景涉及：

1. 从实时相机源中获取相机图像。

1. 使用 Google 机器学习套件将其分为前景和背景分量。

1. 将生成的分割遮罩与自定义背景图像相结合。

1. 将其传递给自定义图像源进行广播。

![\[实现背景替换的工作流程。\]](http://docs.aws.amazon.com/zh_cn/ivs/latest/RealTimeUserGuide/images/3P_Camera_Filters_Background_Replacement.png)


## Web
<a name="background-replacement-web"></a>

本节假设您已经熟悉[使用 Web 广播 SDK 发布和订阅视频](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/getting-started-pub-sub-web.html)。

要使用自定义图像替换直播的背景，请使用带有 [MediaPipe Image Segmenter](https://developers.google.com/mediapipe/solutions/vision/image_segmenter) 的[自拍分割模型](https://developers.google.com/mediapipe/solutions/vision/image_segmenter#selfie-model)。这是一种机器学习模型，用于识别视频帧中的哪些像素位于前景或背景。然后，您可以使用该模型的结果来替换直播的背景，方法是将视频源中的前景像素复制到表示新背景的自定义图像中。

要将背景替换与 IVS 实时流式 Web 广播 SDK 集成，您需要：

1. 安装 MediaPipe 和 Webpack。（我们的示例使用 Webpack 作为捆绑程序，但您可以使用自己选择的任何捆绑程序。）

1. 创建 `index.html`。

1. 添加媒体元素。

1. 添加脚本标签。

1. 创建`app.js`。

1. 加载自定义背景图像。

1. 创建 `ImageSegmenter` 的实例。

1. 将视频源渲染到画布上。

1. 创建背景替换逻辑。

1. 创建 Webpack 配置文件。

1. 捆绑您的 JavaScript 文件。

### 安装 MediaPipe 和 Webpack
<a name="background-replacement-web-install-mediapipe-webpack"></a>

首先，请安装 `@mediapipe/tasks-vision` 和 `webpack` npm 包。下面的示例使用 Webpack 作为 JavaScript 捆绑程序；如果愿意，您可以使用不同的捆绑程序。

#### JavaScript
<a name="background-replacement-web-install-mediapipe-webpack-code"></a>

```
npm i @mediapipe/tasks-vision webpack webpack-cli
```

请务必更新您的 `package.json` 以指定 `webpack` 作为构建脚本：

#### JavaScript
<a name="background-replacement-web-update-package-json-code"></a>

```
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack"
  },
```

### 创建 index.html
<a name="background-replacement-web-create-index"></a>

接下来，创建 HTML 样板，并将 Web 广播 SDK 作为脚本标签导入。在下面的代码中，请务必将 `<SDK version>` 替换为您正在使用的广播 SDK 版本。

#### JavaScript
<a name="background-replacement-web-create-index-code"></a>

```
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <!-- Import the SDK -->
  <script src="https://web-broadcast.live-video.net/<SDK version>/amazon-ivs-web-broadcast.js"></script>
</head>

<body>

</body>
</html>
```

### 添加媒体元素
<a name="background-replacement-web-add-media-elements"></a>

接下来，在正文标签内添加一个视频元素和两个画布元素。视频元素将包含您的实时相机源，并将用作 MediaPipe Image Segmenter 的输入。第一个画布元素将用于渲染将要广播的源的预览。第二个画布元素将用于渲染将用作背景的自定义图像。由于带有自定义图像的第二个画布仅用作以编程方式将像素从其复制到最终画布的来源，因此在视图中被隐藏。

#### JavaScript
<a name="background-replacement-web-add-media-elements-code"></a>

```
<div class="row local-container">
      <video id="webcam" autoplay style="display: none"></video>
    </div>
    <div class="row local-container">
      <canvas id="canvas" width="640px" height="480px"></canvas>

      <div class="column" id="local-media"></div>
      <div class="static-controls hidden" id="local-controls">
        <button class="button" id="mic-control">Mute Mic</button>
        <button class="button" id="camera-control">Mute Camera</button>
      </div>
    </div>
    <div class="row local-container">
      <canvas id="background" width="640px" height="480px" style="display: none"></canvas>
    </div>
```

### 添加脚本标签
<a name="background-replacement-web-add-script-tag"></a>

添加脚本标签以加载捆绑的 JavaScript 文件，该文件将包含用于进行背景替换的代码并将其发布到舞台：

```
<script src="./dist/bundle.js"></script>
```

### 创建 app.js
<a name="background-replacement-web-create-appjs"></a>

接下来，创建一个 JavaScript 文件以获取在 HTML 页面中创建的画布和视频元素的元素对象。导入 `ImageSegmenter` 和 `FilesetResolver` 模块。`ImageSegmenter` 模块将用于执行分割任务。

#### JavaScript
<a name="create-appjs-import-imagesegmenter-fileresolver-code"></a>

```
const canvasElement = document.getElementById("canvas");
const background = document.getElementById("background");
const canvasCtx = canvasElement.getContext("2d");
const backgroundCtx = background.getContext("2d");
const video = document.getElementById("webcam");

import { ImageSegmenter, FilesetResolver } from "@mediapipe/tasks-vision";
```

接下来，创建一个调用 `init()` 的函数，用于从用户的摄像机中检索 MediaStream，并在每次摄像机画面完成加载时调用回调函数。为按钮添加事件侦听器以加入和离开舞台。

请注意，在加入舞台时，我们会传入一个名为 `segmentationStream` 的变量。这是从画布元素捕获的视频流，包含叠加在代表背景的自定义图像上的前景图像。稍后，此自定义流将用于创建 `LocalStageStream` 的实例，该实例可以发布到舞台。

#### JavaScript
<a name="create-appjs-create-init-code"></a>

```
const init = async () => {
  await initializeDeviceSelect();

  cameraButton.addEventListener("click", () => {
    const isMuted = !cameraStageStream.isMuted;
    cameraStageStream.setMuted(isMuted);
    cameraButton.innerText = isMuted ? "Show Camera" : "Hide Camera";
  });

  micButton.addEventListener("click", () => {
    const isMuted = !micStageStream.isMuted;
    micStageStream.setMuted(isMuted);
    micButton.innerText = isMuted ? "Unmute Mic" : "Mute Mic";
  });

  localCamera = await getCamera(videoDevicesList.value);
  const segmentationStream = canvasElement.captureStream();

  joinButton.addEventListener("click", () => {
    joinStage(segmentationStream);
  });

  leaveButton.addEventListener("click", () => {
    leaveStage();
  });
};
```

### 加载自定义背景图像
<a name="background-replacement-web-background-image"></a>

在 `init` 函数的底部，添加用于调用名为 `initBackgroundCanvas` 的函数的代码，该函数从本地文件加载自定义图像并将其渲染到画布上。我们将在下一个步骤中定义此函数。将从用户相机检索到的 `MediaStream` 分配给视频对象。稍后，该视频对象将传递到 Image Segmenter。此外，还要设置一个名为 `renderVideoToCanvas` 的函数作为回调函数，以便在视频帧加载完毕时调用。我们将在稍后的步骤中定义此函数。

#### JavaScript
<a name="background-replacement-web-load-background-image-code"></a>

```
initBackgroundCanvas();

  video.srcObject = localCamera;
  video.addEventListener("loadeddata", renderVideoToCanvas);
```

让我们实现 `initBackgroundCanvas` 函数，它从本地文件加载图像。在此示例中，我们使用一张海滩的图像作为自定义背景。包含自定义图像的画布将从显示画面中隐藏，因为您将把它与包含相机源的画布元素的前景像素合并。

#### JavaScript
<a name="background-replacement-web-implement-initBackgroundCanvas-code"></a>

```
const initBackgroundCanvas = () => {
  let img = new Image();
  img.src = "beach.jpg";

  img.onload = () => {
    backgroundCtx.clearRect(0, 0, canvas.width, canvas.height);
    backgroundCtx.drawImage(img, 0, 0);
  };
};
```

### 创建 ImageSegmenter 实例
<a name="background-replacement-web-imagesegmenter"></a>

接下来，创建 `ImageSegmenter` 的实例，该实例将对图像进行分割并返回结果作为遮罩。在创建 `ImageSegmenter` 的实例时，您将使用[自拍分割模型](https://developers.google.com/mediapipe/solutions/vision/image_segmenter#selfie-model)。

#### JavaScript
<a name="background-replacement-web-imagesegmenter-code"></a>

```
const createImageSegmenter = async () => {
  const audio = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.2/wasm");

  imageSegmenter = await ImageSegmenter.createFromOptions(audio, {
    baseOptions: {
      modelAssetPath: "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite",
      delegate: "GPU",
    },
    runningMode: "VIDEO",
    outputCategoryMask: true,
  });
};
```

### 将视频源渲染到画布上
<a name="background-replacement-web-render-video-to-canvas"></a>

接下来，创建将视频源渲染到其他画布元素的函数。我们需要将视频源渲染到画布上，这样我们就可以使用 Canvas 2D API 从画布中提取前景像素。在执行此操作时，我们还会将视频帧传递给我们的 `ImageSegmenter` 实例，使用 [segmentforVideo](https://developers.google.com/mediapipe/api/solutions/js/tasks-vision.imagesegmenter#imagesegmentersegmentforvideo) 方法在视频帧中分割前景与背景。当 [segmentforVideo](https://developers.google.com/mediapipe/api/solutions/js/tasks-vision.imagesegmenter#imagesegmentersegmentforvideo) 方法返回时，它会调用我们的自定义回调函数 `replaceBackground` 来进行背景替换。

#### JavaScript
<a name="background-replacement-web-render-video-to-canvas-code"></a>

```
const renderVideoToCanvas = async () => {
  if (video.currentTime === lastWebcamTime) {
    window.requestAnimationFrame(renderVideoToCanvas);
    return;
  }
  lastWebcamTime = video.currentTime;
  canvasCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);

  if (imageSegmenter === undefined) {
    return;
  }

  let startTimeMs = performance.now();

  imageSegmenter.segmentForVideo(video, startTimeMs, replaceBackground);
};
```

### 创建背景替换逻辑
<a name="background-replacement-web-logic"></a>

创建 `replaceBackground` 函数，它可将自定义背景图像与相机视频源中的前景合并，以替换背景。该函数首先从此前创建的两个画布元素中检索自定义背景图像和视频源的底层像素数据。然后，它会遍历 `ImageSegmenter` 提供的遮罩，该遮罩指示前景中有哪些像素。当它遍历遮罩时，会有选择地将包含用户相机源的像素复制到相应的背景像素数据中。完成后，它会对最终的像素数据进行转换，并将前景复制到背景上，然后将其绘制到画布上。

#### JavaScript
<a name="background-replacement-web-logic-create-replacebackground-code"></a>

```
function replaceBackground(result) {
  let imageData = canvasCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
  let backgroundData = backgroundCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
  const mask = result.categoryMask.getAsFloat32Array();
  let j = 0;

  for (let i = 0; i < mask.length; ++i) {
    const maskVal = Math.round(mask[i] * 255.0);

    j += 4;
  // Only copy pixels on to the background image if the mask indicates they are in the foreground
    if (maskVal < 255) {
      backgroundData[j] = imageData[j];
      backgroundData[j + 1] = imageData[j + 1];
      backgroundData[j + 2] = imageData[j + 2];
      backgroundData[j + 3] = imageData[j + 3];
    }
  }

 // Convert the pixel data to a format suitable to be drawn to a canvas
  const uint8Array = new Uint8ClampedArray(backgroundData.buffer);
  const dataNew = new ImageData(uint8Array, video.videoWidth, video.videoHeight);
  canvasCtx.putImageData(dataNew, 0, 0);
  window.requestAnimationFrame(renderVideoToCanvas);
}
```

作为参考，下面是包含上述所有逻辑的完整 `app.js` 文件：

#### JavaScript
<a name="background-replacement-web-logic-app-js-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

// All helpers are expose on 'media-devices.js' and 'dom.js'
const { setupParticipant } = window;

const { Stage, LocalStageStream, SubscribeType, StageEvents, ConnectionState, StreamType } = IVSBroadcastClient;
const canvasElement = document.getElementById("canvas");
const background = document.getElementById("background");
const canvasCtx = canvasElement.getContext("2d");
const backgroundCtx = background.getContext("2d");
const video = document.getElementById("webcam");

import { ImageSegmenter, FilesetResolver } from "@mediapipe/tasks-vision";

let cameraButton = document.getElementById("camera-control");
let micButton = document.getElementById("mic-control");
let joinButton = document.getElementById("join-button");
let leaveButton = document.getElementById("leave-button");

let controls = document.getElementById("local-controls");
let audioDevicesList = document.getElementById("audio-devices");
let videoDevicesList = document.getElementById("video-devices");

// Stage management
let stage;
let joining = false;
let connected = false;
let localCamera;
let localMic;
let cameraStageStream;
let micStageStream;
let imageSegmenter;
let lastWebcamTime = -1;

const init = async () => {
  await initializeDeviceSelect();

  cameraButton.addEventListener("click", () => {
    const isMuted = !cameraStageStream.isMuted;
    cameraStageStream.setMuted(isMuted);
    cameraButton.innerText = isMuted ? "Show Camera" : "Hide Camera";
  });

  micButton.addEventListener("click", () => {
    const isMuted = !micStageStream.isMuted;
    micStageStream.setMuted(isMuted);
    micButton.innerText = isMuted ? "Unmute Mic" : "Mute Mic";
  });

  localCamera = await getCamera(videoDevicesList.value);
  const segmentationStream = canvasElement.captureStream();

  joinButton.addEventListener("click", () => {
    joinStage(segmentationStream);
  });

  leaveButton.addEventListener("click", () => {
    leaveStage();
  });

  initBackgroundCanvas();

  video.srcObject = localCamera;
  video.addEventListener("loadeddata", renderVideoToCanvas);
};

const joinStage = async (segmentationStream) => {
  if (connected || joining) {
    return;
  }
  joining = true;

  const token = document.getElementById("token").value;

  if (!token) {
    window.alert("Please enter a participant token");
    joining = false;
    return;
  }

  // Retrieve the User Media currently set on the page
  localMic = await getMic(audioDevicesList.value);

  cameraStageStream = new LocalStageStream(segmentationStream.getVideoTracks()[0]);
  micStageStream = new LocalStageStream(localMic.getAudioTracks()[0]);

  const strategy = {
    stageStreamsToPublish() {
      return [cameraStageStream, micStageStream];
    },
    shouldPublishParticipant() {
      return true;
    },
    shouldSubscribeToParticipant() {
      return SubscribeType.AUDIO_VIDEO;
    },
  };

  stage = new Stage(token, strategy);

  // Other available events:
  // https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#events
  stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {
    connected = state === ConnectionState.CONNECTED;

    if (connected) {
      joining = false;
      controls.classList.remove("hidden");
    } else {
      controls.classList.add("hidden");
    }
  });

  stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {
    console.log("Participant Joined:", participant);
  });

  stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => {
    console.log("Participant Media Added: ", participant, streams);

    let streamsToDisplay = streams;

    if (participant.isLocal) {
      // Ensure to exclude local audio streams, otherwise echo will occur
      streamsToDisplay = streams.filter((stream) => stream.streamType === StreamType.VIDEO);
    }

    const videoEl = setupParticipant(participant);
    streamsToDisplay.forEach((stream) => videoEl.srcObject.addTrack(stream.mediaStreamTrack));
  });

  stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant) => {
    console.log("Participant Left: ", participant);
    teardownParticipant(participant);
  });

  try {
    await stage.join();
  } catch (err) {
    joining = false;
    connected = false;
    console.error(err.message);
  }
};

const leaveStage = async () => {
  stage.leave();

  joining = false;
  connected = false;

  cameraButton.innerText = "Hide Camera";
  micButton.innerText = "Mute Mic";
  controls.classList.add("hidden");
};

function replaceBackground(result) {
  let imageData = canvasCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
  let backgroundData = backgroundCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data;
  const mask = result.categoryMask.getAsFloat32Array();
  let j = 0;

  for (let i = 0; i < mask.length; ++i) {
    const maskVal = Math.round(mask[i] * 255.0);

    j += 4;
    if (maskVal < 255) {
      backgroundData[j] = imageData[j];
      backgroundData[j + 1] = imageData[j + 1];
      backgroundData[j + 2] = imageData[j + 2];
      backgroundData[j + 3] = imageData[j + 3];
    }
  }
  const uint8Array = new Uint8ClampedArray(backgroundData.buffer);
  const dataNew = new ImageData(uint8Array, video.videoWidth, video.videoHeight);
  canvasCtx.putImageData(dataNew, 0, 0);
  window.requestAnimationFrame(renderVideoToCanvas);
}

const createImageSegmenter = async () => {
  const audio = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.2/wasm");

  imageSegmenter = await ImageSegmenter.createFromOptions(audio, {
    baseOptions: {
      modelAssetPath: "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite",
      delegate: "GPU",
    },
    runningMode: "VIDEO",
    outputCategoryMask: true,
  });
};

const renderVideoToCanvas = async () => {
  if (video.currentTime === lastWebcamTime) {
    window.requestAnimationFrame(renderVideoToCanvas);
    return;
  }
  lastWebcamTime = video.currentTime;
  canvasCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);

  if (imageSegmenter === undefined) {
    return;
  }

  let startTimeMs = performance.now();

  imageSegmenter.segmentForVideo(video, startTimeMs, replaceBackground);
};

const initBackgroundCanvas = () => {
  let img = new Image();
  img.src = "beach.jpg";

  img.onload = () => {
    backgroundCtx.clearRect(0, 0, canvas.width, canvas.height);
    backgroundCtx.drawImage(img, 0, 0);
  };
};

createImageSegmenter();
init();
```

### 创建 Webpack 配置文件
<a name="background-replacement-web-webpack-config"></a>

将此配置添加到要捆绑 `app.js` 的 Webpack 配置文件中，这样导入调用就会起作用：

#### JavaScript
<a name="background-replacement-web-webpack-config-code"></a>

```
const path = require("path");
module.exports = {
  entry: ["./app.js"],
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
};
```

### 捆绑您的 JavaScript 文件
<a name="background-replacement-web-bundle-javascript"></a>

```
npm run build
```

从包含 `index.html` 的目录中启动简单 HTTP 服务器并打开 `localhost:8000` 以查看结果：

```
python3 -m http.server -d ./
```

## Android
<a name="background-replacement-android"></a>

要替换直播中的背景，可以使用 [Google 机器学习套件](https://developers.google.com/ml-kit/vision/selfie-segmentation)的自拍分割 API。自拍分割 API 接受相机图像作为输入，并返回一个遮罩，该遮罩为图像的每个像素提供置信度分数，指示图像是在前景还是背景中。根据置信度分数，您可以从背景图像或前景图像检索相应的像素颜色。此流程一直持续到检查完遮罩中的所有置信度分数为止。结果是一个新像素颜色数组，其中包含前景像素以及来自背景图像的像素。

要将背景替换与 IVS 实时流式 Android 广播 SDK 集成，您需要：

1. 安装 CameraX 库和 Google 机器学习套件。

1. 初始化样板变量。

1. 创建自定义图像源。

1. 管理相机帧。

1. 将相机帧传递到 Google 机器学习套件。

1. 将相机帧前景叠加到您的自定义背景上。

1. 将新图像馈送到自定义图像源。

### 安装 CameraX 库和 Google 机器学习套件
<a name="background-replacement-android-install-camerax-googleml"></a>

要从实时相机源中提取图像，请使用 Android 的 CameraX 库。要安装 CameraX 库和 Google 机器学习套件，请将以下内容添加到您的模块的 `build.gradle` 文件中。分别将 `${camerax_version}` 和 `${google_ml_kit_version}` 替换为最新版本的 [CameraX](https://developer.android.com/jetpack/androidx/releases/camera) 和 [Google 机器学习套件](https://developers.google.com/ml-kit/vision/selfie-segmentation/android)库。

#### Java
<a name="background-replacement-android-install-camerax-googleml-code"></a>

```
implementation "com.google.mlkit:segmentation-selfie:${google_ml_kit_version}"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
```

导入以下库：

#### Java
<a name="background-replacement-android-import-libraries-code"></a>

```
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider
import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions
```

### 初始化样板变量
<a name="background-replacement-android-initialize-variables"></a>

初始化 `ImageAnalysis` 的实例和 `ExecutorService` 的实例：

#### Java
<a name="background-replacement-android-initialize-imageanalysis-executorservice-code"></a>

```
private lateinit var binding: ActivityMainBinding
private lateinit var cameraExecutor: ExecutorService
private var analysisUseCase: ImageAnalysis? = null
```

在 [STREAM\$1MODE](https://developers.google.com/ml-kit/vision/selfie-segmentation/android#detector_mode) 中初始化 Segmenter 实例：

#### Java
<a name="background-replacement-android-initialize-segmenter-code"></a>

```
private val options =
        SelfieSegmenterOptions.Builder()
            .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
            .build()

private val segmenter = Segmentation.getClient(options)
```

### 创建自定义图像源
<a name="background-replacement-android-create-image-source"></a>

在活动的 `onCreate` 方法中，创建 `DeviceDiscovery` 对象的实例并创建自定义图像源。自定义图像源提供的 `Surface` 将收到最终图像，前景叠加在自定义背景图像上。然后，您将使用自定义图像源创建 `ImageLocalStageStream` 的实例。然后，可以将 `ImageLocalStageStream`（在此例中名为 `filterStream`）的实例发布到舞台。有关设置舞台的说明，请参阅 [IVS Android 广播 SDK 指南](broadcast-android.md)。最后，还要创建一个用于管理相机的线程。

#### Java
<a name="background-replacement-android-create-image-source-code"></a>

```
var deviceDiscovery = DeviceDiscovery(applicationContext)
var customSource = deviceDiscovery.createImageInputSource( BroadcastConfiguration.Vec2(
720F, 1280F
))
var surface: Surface = customSource.inputSurface
var filterStream = ImageLocalStageStream(customSource)

cameraExecutor = Executors.newSingleThreadExecutor()
```

### 管理相机帧
<a name="background-replacement-android-camera-frames"></a>

接下来，创建一个函数来初始化相机。此功能使用 CameraX 库从实时相机源中提取图像。首先，创建调用 `cameraProviderFuture` 的 `ProcessCameraProvider` 的实例。此对象表示获取摄像机提供者操作的未来结果。然后，将项目中的图像作为位图加载。此示例使用海滩图像作为背景，但它可以是您想使用的任何图像。

然后，向 `cameraProviderFuture` 添加一个侦听器。当相机可用时，或者在获取相机提供者的过程中出现错误时，系统会通知侦听器。

#### Java
<a name="background-replacement-android-initialize-camera-code"></a>

```
private fun startCamera(surface: Surface) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        val imageResource = R.drawable.beach
        val bgBitmap: Bitmap = BitmapFactory.decodeResource(resources, imageResource)
        var resultBitmap: Bitmap;


        cameraProviderFuture.addListener({
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            
                if (mediaImage != null) {
                    val inputImage =
                        InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

                            resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
                            canvas = surface.lockCanvas(null);
                            canvas.drawBitmap(resultBitmap, 0f, 0f, null)

                            surface.unlockCanvasAndPost(canvas);

                        }
                        .addOnFailureListener { exception ->
                            Log.d("App", exception.message!!)
                        }
                        .addOnCompleteListener {
                            imageProxy.close()
                        }

                }
            };

            val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase)

            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }
```

在侦听器中，创建 `ImageAnalysis.Builder` 以访问来自实时相机源的每个帧。将反向压力策略设置为 `STRATEGY_KEEP_ONLY_LATEST`。这样可以保证一次只能传输一个相机帧进行处理。将每个相机帧转换为位图，这样您就可以提取其像素，以便稍后将其与自定义背景图像合并。

#### Java
<a name="background-replacement-android-create-imageanalysisbuilder-code"></a>

```
val imageAnalyzer = ImageAnalysis.Builder()
analysisUseCase = imageAnalyzer
    .setTargetResolution(Size(360, 640))
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()

analysisUseCase?.setAnalyzer(cameraExecutor) { imageProxy: ImageProxy ->
    val mediaImage = imageProxy.image
    val tempBitmap = imageProxy.toBitmap();
    val inputBitmap = tempBitmap.rotate(imageProxy.imageInfo.rotationDegrees.toFloat())
```

### 将相机帧传递到 Google 机器学习套件
<a name="background-replacement-android-frames-to-mlkit"></a>

接下来，创建一个 `InputImage` 并将其传递给 Segmenter 的实例进行处理。`InputImage` 可以从 `ImageAnalysis` 的实例提供的 `ImageProxy` 创建。向 Segmenter 提供 `InputImage` 后，它将返回一个遮罩，其置信度分数表示像素出现在前景或背景中的可能性。此遮罩还提供宽度和高度属性，您将使用这些属性来创建一个包含先前加载的自定义背景图像中的背景像素的新数组。

#### Java
<a name="background-replacement-android-frames-to-mlkit-code"></a>

```
if (mediaImage != null) {
        val inputImage =
            InputImage.fromMediaImag


segmenter.process(inputImage)
    .addOnSuccessListener { segmentationMask ->
        val mask = segmentationMask.buffer
        val maskWidth = segmentationMask.width
        val maskHeight = segmentationMask.height
        val backgroundPixels = IntArray(maskWidth * maskHeight)
        bgBitmap.getPixels(backgroundPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)
```

### 将相机帧前景叠加到您的自定义背景上
<a name="background-replacement-android-overlay-frame-foreground"></a>

借助包含置信度分数的遮罩、作为位图的相机帧以及自定义背景图像中的彩色像素，您可以拥有将前景叠加到自定义背景所需的一切。然后使用以下参数调用 `overlayForeground` 函数：

#### Java
<a name="background-replacement-android-call-overlayforeground-code"></a>

```
resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
```

此函数遍历遮罩并检查置信度值，以确定是从背景图像还是从相机帧中获取相应的像素颜色。如果置信度值表明遮罩中的像素很可能位于背景中，则将从背景图像中获得相应的像素颜色；否则，它将从相机帧中获取相应的像素颜色来构建前景。函数完成对遮罩的遍历后，将使用新的彩色像素数组创建一个新的位图并返回。这个新位图包含叠加在自定义背景上的前景。

#### Java
<a name="background-replacement-android-run-overlayforeground-code"></a>

```
private fun overlayForeground(
        byteBuffer: ByteBuffer,
        maskWidth: Int,
        maskHeight: Int,
        cameraBitmap: Bitmap,
        backgroundPixels: IntArray
    ): Bitmap {
        @ColorInt val colors = IntArray(maskWidth * maskHeight)
        val cameraPixels = IntArray(maskWidth * maskHeight)

        cameraBitmap.getPixels(cameraPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)

        for (i in 0 until maskWidth * maskHeight) {
            val backgroundLikelihood: Float = 1 - byteBuffer.getFloat()

            // Apply the virtual background to the color if it's not part of the foreground
            if (backgroundLikelihood > 0.9) {
                // Get the corresponding pixel color from the background image
                // Set the color in the mask based on the background image pixel color
                colors[i] = backgroundPixels.get(i)
            } else {
                // Get the corresponding pixel color from the camera frame
                // Set the color in the mask based on the camera image pixel color
                colors[i] = cameraPixels.get(i)
            }
        }

        return Bitmap.createBitmap(
            colors, maskWidth, maskHeight, Bitmap.Config.ARGB_8888
        )
    }
```

### 将新图像馈送到自定义图像源
<a name="background-replacement-android-custom-image-source"></a>

然后，您可以将新位图写入到自定义图像源提供的 `Surface`。这将把它广播到您的舞台。

#### Java
<a name="background-replacement-android-custom-image-source-code"></a>

```
resultBitmap = overlayForeground(mask, inputBitmap, mutableBitmap, bgBitmap)
canvas = surface.lockCanvas(null);
canvas.drawBitmap(resultBitmap, 0f, 0f, null)
```

下面是获取相机帧、将其传递给 Segmenter 并叠加到背景上的完整功能：

#### Java
<a name="background-replacement-android-custom-image-source-startcamera-code"></a>

```
@androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class)
    private fun startCamera(surface: Surface) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        val imageResource = R.drawable.clouds
        val bgBitmap: Bitmap = BitmapFactory.decodeResource(resources, imageResource)
        var resultBitmap: Bitmap;

        cameraProviderFuture.addListener({
            // Used to bind the lifecycle of cameras to the lifecycle owner
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            val imageAnalyzer = ImageAnalysis.Builder()
            analysisUseCase = imageAnalyzer
                .setTargetResolution(Size(720, 1280))
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()

            analysisUseCase!!.setAnalyzer(cameraExecutor) { imageProxy: ImageProxy ->
                val mediaImage = imageProxy.image
                val tempBitmap = imageProxy.toBitmap();
                val inputBitmap = tempBitmap.rotate(imageProxy.imageInfo.rotationDegrees.toFloat())

                if (mediaImage != null) {
                    val inputImage =
                        InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

                    segmenter.process(inputImage)
                        .addOnSuccessListener { segmentationMask ->
                            val mask = segmentationMask.buffer
                            val maskWidth = segmentationMask.width
                            val maskHeight = segmentationMask.height
                            val backgroundPixels = IntArray(maskWidth * maskHeight)
                            bgBitmap.getPixels(backgroundPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)

                            resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
                            canvas = surface.lockCanvas(null);
                            canvas.drawBitmap(resultBitmap, 0f, 0f, null)

                            surface.unlockCanvasAndPost(canvas);

                        }
                        .addOnFailureListener { exception ->
                            Log.d("App", exception.message!!)
                        }
                        .addOnCompleteListener {
                            imageProxy.close()
                        }

                }
            };

            val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase)

            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }
```

# IVS 广播 SDK：移动音频模式 \$1 实时直播功能
<a name="broadcast-mobile-audio-modes"></a>

音频质量是任何真实团队媒体体验的重要组成部分，而且没有适合所有使用案例的“一刀切”音频配置。为了确保您的用户在收听 IVS 直播时获得最佳体验，我们的移动 SDK 提供了多种预设音频配置，并可根据需要提供更强大的自定义设置。

## 简介
<a name="broadcast-mobile-audio-modes-introduction"></a>

IVS 移动广播 SDK 提供了一个 `StageAudioManager` 类。该类旨在成为控制两种平台上的底层音频模式的单一接触点。在 Android 上，它控制 [AudioManager](https://developer.android.com/reference/android/media/AudioManager)，包括音频模式、音频源、内容类型、使用情况和通信设备。在 iOS 上，它控制应用程序 [AVAudioSession](https://developer.apple.com/documentation/avfaudio/avaudiosession)，以及 [voiceProcessing](https://developer.apple.com/documentation/avfaudio/avaudioionode/3152101-voiceprocessingenabled?language=objc) 是否已启用。

**重要提示**：在 IVS 实时广播 SDK 处于活动状态时，请勿与 `AVAudioSession` 或 `AudioManager` 直接交互。那样可能会导致音频丢失，或者在错误的设备上录制或播放音频。

在创建第一个 `DeviceDiscovery` 或 `Stage` 对象之前，必须配置 `StageAudioManager` 类。

------
#### [ Android (Kotlin) ]

```
StageAudioManager.getInstance(context).setPreset(StageAudioManager.UseCasePreset.VIDEO_CHAT) // The default value

val deviceDiscovery = DeviceDiscovery(context)
val stage = Stage(context, token, this)

// Other Stage implementation code
```

------
#### [ iOS (Swift) ]

```
IVSStageAudioManager.sharedInstance().setPreset(.videoChat) // The default value

let deviceDiscovery = IVSDeviceDiscovery()
let stage = try? IVSStage(token: token, strategy: self)

// Other Stage implementation code
```

------

如果在初始化 `DeviceDiscovery` 或 `Stage` 实例之前未在 `StageAudioManager` 上设置任何内容，则会自动应用 `VideoChat` 预设。

## 音频模式预设
<a name="broadcast-mobile-audio-modes-presets"></a>

实时广播 SDK 提供了三种预设，各自针对常见使用案例量身定制，如下文所述。对于每种预设，我们涵盖了五个关键类别，用于将预设相互区分开来。

**音量摇杆**类别是指通过设备上的物理音量摇杆使用或更改的音量类型（媒体音量或通话音量）。请注意，在切换音频模式时，这会影响音量。例如，假设在使用视频聊天预设时将设备音量设置为最大值。切换到“仅订阅”预设会导致与操作系统不同的音量，这可能会导致设备上的音量发生显著变化。

### 视频聊天
<a name="audio-modes-presets-video-chat"></a>

这是默认预设，专为本地设备与其他参与者进行实时对话的应用场景设计。

**iOS 上的已知问题**：使用此预设而不连接麦克风会导致音频通过耳机而不是设备扬声器播放。此预设只能与麦克风组合使用。


| 类别 | Android | iOS | 
| --- | --- | --- | 
| 回声消除 | 已启用 | 已启用 | 
| 音量摇杆 | 通话音量 | 通话音量 | 
| 麦克风选择 | 视操作系统进行限制。USB 麦克风可能不可用。 | 视操作系统进行限制。USB 和蓝牙麦克风可能不可用。 同时处理输入和输出的蓝牙耳机应可正常工作；例如 AirPods。 | 
| 音频输出 | 任何输出设备都应正常工作。 | 视操作系统进行限制。有线耳机可能不可用。 | 
| 音频质量 | 中/低。听起来像是打电话，不像播放媒体。 | 中/低。听起来像是打电话，不像播放媒体。 | 

### 仅订阅
<a name="audio-modes-presets-subscribe-only"></a>

此预设适合用于您计划订阅其他发布参与者但不打算自己发布的情况。它专注于音频质量并支持所有可用的输出设备。


| 类别 | Android | iOS | 
| --- | --- | --- | 
| 回声消除 | 已禁用 | 已禁用 | 
| 音量摇杆 | 媒体音量 | 媒体音量 | 
| 麦克风选择 | 不适用，此预设不适合用于发布。 | 不适用，此预设不适合用于发布。 | 
| 音频输出 | 任何输出设备都应正常工作。 | 任何输出设备都应正常工作。 | 
| 音频质量 | 高。任何媒体类型都应清晰，包括音乐。 | 高。任何媒体类型都应清晰，包括音乐。 | 

### Studio
<a name="audio-modes-presets-studio"></a>

此预设专为高质量订阅设计，同时保持发布能力。它需要录制和播放硬件来消除回声。此处的使用案例是使用 USB 麦克风和有线耳机。该 SDK 将保持最高质量的音频，同时依赖这些设备的物理隔离，以免产生回声。


| 类别 | Android | iOS | 
| --- | --- | --- | 
| 回声消除 | 平台回声消除已禁用，但如果 `StageAudioConfiguration.enableEchoCancellation` 为 true，则仍可能会发生软件回声消除。 | 已禁用 | 
| 音量摇杆 | 大多数情况下的媒体音量。连接蓝牙麦克风时的通话音量。 | 媒体音量 | 
| 麦克风选择 | 任何麦克风都应正常工作。 | 任何麦克风都应正常工作。 | 
| 音频输出 | 任何输出设备都应正常工作。 | 任何输出设备都应正常工作。 | 
| 音频质量 | 高。双方都应能够发送音乐，另一方也能清晰地听见。 连接蓝牙耳机后，由于启用了蓝牙 SCO 模式，音频质量将会下降。 | 高。双方都应能够发送音乐，另一方也能清晰地听见。 连接蓝牙耳机后，由于启用了蓝牙 SCO 模式，音频质量可能会下降，这取决于耳机。 | 

## 高级使用案例
<a name="broadcast-mobile-audio-modes-advanced-use-cases"></a>

除了预设之外，iOS 和 Android 实时流式广播 SDK 都允许配置底层平台的音频模式：
+ 在 Android 上，设置 [AudioSource](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource)、[Usage](https://developer.android.com/reference/android/media/AudioAttributes#USAGE_ALARM) 和 [ContentType](https://developer.android.com/reference/android/media/AudioAttributes#CONTENT_TYPE_MOVIE)。
+ 在 iOS 上，使用 [AVAudioSession.Category](https://developer.apple.com/documentation/avfaudio/avaudiosession/category)、[AVAudioSession.CategoryOptions](https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions)、[AVAudioSession.Mode](https://developer.apple.com/documentation/avfaudio/avaudiosession/mode)，以及在发布时切换是否启用[语音处理](https://developer.apple.com/documentation/avfaudio/avaudioionode/3152101-voiceprocessingenabled?language=objc)的功能。

注意：使用这些音频 SDK 方法时，可能会错误地配置底层音频会话。例如，在 iOS 上将 `.allowBluetooth` 选项与 `.playback` 类别结合使用会产生无效的音频配置，并且 SDK 无法录制或播放音频。这些方法仅在应用程序具有经过验证的特定音频会话要求时使用。

------
#### [ Android (Kotlin) ]

```
// This would act similar to the Subscribe Only preset, but it uses a different ContentType.
StageAudioManager.getInstance(context)
    .setConfiguration(StageAudioManager.Source.GENERIC,
                      StageAudioManager.ContentType.MOVIE,
                      StageAudioManager.Usage.MEDIA);

val stage = Stage(context, token, this)

// Other Stage implementation code
```

------
#### [ iOS (Swift) ]

```
// This would act similar to the Subscribe Only preset, but it uses a different mode and options.
IVSStageAudioManager.sharedInstance()
    .setCategory(.playback,
                 options: [.duckOthers, .mixWithOthers],
                 mode: .default)

let stage = try? IVSStage(token: token, strategy: self)

// Other Stage implementation code
```

------

### iOS 回声消除
<a name="advanced-use-cases-ios_echo_cancellation"></a>

iOS 上的回声消除也可以通过 `IVSStageAudioManager` 使用其 `echoCancellationEnabled` 方法独立控制。此方法控制是否在 SDK 使用的底层 `AVAudioEngine` 的输入和输出节点上启用[语音处理](https://developer.apple.com/documentation/avfaudio/avaudioionode/3152101-voiceprocessingenabled?language=objc)。了解手动更改此属性的影响非常重要：
+ 只有当 SDK 的麦克风处于活动状态时，`AVAudioEngine` 属性才会生效；这是因为 iOS 要求同时在输入和输出节点上启用语音处理，因此这一点很有必要。通常，这会通过使用 `IVSDeviceDiscovery` 返回的麦克风创建要发布的 `IVSLocalStageStream` 来完成。或者，也可以通过将 `IVSAudioDeviceStatsCallback` 附加到麦克风本身来启用麦克风，而无需将其用于发布。如果在使用基于音频源的自定义麦克风而不是 IVS SDK 的麦克风时需要回声消除，则这种替代方法非常有用。
+ 启用 `AVAudioEngine` 属性需要 `.videoChat` 或 `.voiceChat` 模式。请求不同的模式会导致 iOS 的底层音频框架与 SDK 发生冲突，从而导致音频丢失。
+ 启用 `AVAudioEngine` 会自动启用 `.allowBluetooth ` 选项。

行为可能因设备和 iOS 版本不同而有所不同。

### iOS 自定义音频源
<a name="advanced-use-cases-ios_custom_audio_sources"></a>

通过使用 `IVSDeviceDiscovery.createAudioSource`，可以将自定义音频源与 SDK 配合使用。连接到暂存区时，即使未使用 SDK 的麦克风，IVS 实时流式广播 SDK 仍会管理用于音频播放的内部 `AVAudioEngine` 实例。因此，提供给 `IVSStageAudioManager` 的值必须与自定义音频源提供的音频兼容。

如果用于发布的自定义音频源是通过麦克风录制但由主机应用程序管理，则除非激活 SDK 管理的麦克风，否则上述回声消除 SDK 将无法运行。要绕过该要求，请参阅 [iOS 回声消除](#advanced-use-cases-ios_echo_cancellation)。

### 在 Android 系统上使用蓝牙发布
<a name="advanced-use-cases-bluetooth-android"></a>

满足以下条件时，SDK 将自动恢复为 Android 上的 `VIDEO_CHAT` 预设：
+ 分配的配置不使用 `VOICE_COMMUNICATION` 使用值。
+ 蓝牙麦克风已连接到设备。
+ 本地参与者正在向舞台发布内容。

这是 Android 操作系统在如何使用蓝牙耳机录制音频方面的限制。

## 与其他 SDK 集成
<a name="broadcast-mobile-audio-modes-integrating-other-sdks"></a>

由于 iOS 和 Android 对每个应用都仅支持一种活动音频模式，因此如果您的应用使用多个需要控制音频模式的 SDK，则经常会遇到冲突。当您遇到这些冲突时，可以尝试一些常见的解决策略，如下所述。

### 匹配音频模式值
<a name="integrating-other-sdks-match-values"></a>

使用 IVS SDK 的高级音频配置选项或其他 SDK 的功能，使这两种 SDK 在底层值上保持一致。

### Agora
<a name="integrating-other-sdks-agora"></a>

#### iOS
<a name="integrating-other-sdks-agora-ios"></a>

在 iOS 上，让 Agora SDK 将 `AVAudioSession` 保持活动状态可防止在 IVS 实时流式广播 SDK 使用它时它被停用。

```
myRtcEngine.SetParameters("{\"che.audio.keep.audiosession\":true}");
```

#### Android
<a name="integrating-other-sdks-agora-android"></a>

使用 IVS 实时流式广播 SDK 时，避免对 `RtcEngine` 调用 `setEnableSpeakerphone`，而应调用 `enableLocalAudio(false)`。当 IVS SDK 未发布时，您可以再次调用 `enableLocalAudio(true)`。