

# 使用 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);
```