

# 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 会自动重试。或者，应用程序可以刷新策略以强制进行更多重试。如果连接完全丢失，指向阶段的连接也可能失败。