

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

`Stage` 類別也可連接 `StageRenderer`：

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

### 策略
<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);
}
```

發布時，這會用來決定應發布哪些音訊和影片串流。稍後會在[發布媒體串流](#android-publish-subscribe-publish-stream)中進行詳細說明。

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

策略應處於動態狀態：從上述任何函數返回的值可以隨時進行修改。例如，若主持人應用程式在終端使用者按下按鈕前都不想發布，您可以從 `shouldPublishFromParticipant` 傳回一個變數 (例如 `hasUserTappedPublishButton`)。當該變數根據終端使用者的互動而變更時，請呼叫 `stage.refreshStrategy()` 向 SDK 傳送訊號，表示它應查詢策略中的最新值，並僅套用已變更的項目。若 SDK 發現 `shouldPublishFromParticipant` 值已變更，它便會開始發布程序。若 SDK 查詢後所有函數傳回與之前相同的值，則 `refreshStrategy` 呼叫將不會對階段進行任何修改。

若 `shouldSubscribeToParticipant` 傳回的值從 `AUDIO_VIDEO` 變更為 `AUDIO_ONLY`，則系統將會針對傳回值已變更的所有參與者移除影片串流 (若之前存有影片串流)。

一般而言，階段會採用策略，以最有效率的方式套用先前與目前策略之間的差異，主持人應用程式不必擔心正確進行管理所需的所有狀態。因此，請將呼叫 `stage.refreshStrategy()` 視為低成本的操作，因為除非策略發生變化，否則它什麼都不會執行。

### 轉譯器
<a name="android-publish-subscribe-concepts-renderer"></a>

`StageRenderer` 介面會將階段狀態傳送給主持人應用程式。主持人應用程式的 UI 更新通常可以完全由轉譯器提供的事件提供支援。轉譯器會提供以下函數：

```
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` 會在所有情況下調用，因此在遠端或本機離開操作期間，不需要使用自訂商業邏輯從 UI 移除參與者。

## 靜音和取消靜音媒體串流
<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` 方法來據此更新您的 UI。

```
@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` 設定的 `StageStream.Listener` 收到統計資料。

```
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` 方法，將訊息承載嵌入至其影片串流。承載大小必須大於 0KB，小於 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();
}
```

## Simulcast 分層編碼
<a name="android-publish-subscribe-layered-encoding-simulcast"></a>

Simulcast 分層編碼是一種 IVS 即時串流功能，可讓發布者傳送多個不同品質的影片層，也可讓訂閱用戶動態或手動設定這些層。[串流最佳化](real-time-streaming-optimization.md)文件中詳細介紹了該功能。

### 設定分層編碼 (發布者)
<a name="android-layered-encoding-simulcast-configure-publisher"></a>

若要以發布者身分啟用 Simulcast 分層編碼，請在執行個體化時將下列組態新增至 `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)小節中的定義，來編碼和傳送一定數量的層。

此外，您也可選擇從 Simulcast 組態內設定個別層：

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

或者，您可以建立自訂層組態，最多三層。如果您提供空陣列或未提供任何值，則會使用上述預設值。透過下列必要屬性設定來描述層：
+ `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>

訂閱用戶無需執行任何操作來啟用分層編碼。如果發布者正在傳送 Simulcast 層，則伺服器預設會在各層之間動態調整，根據訂閱用戶的裝置和網路狀況選擇品質最佳的層。

或者，若要挑選發布者正在傳送的明確層，有幾個選項可供選擇，如下所述。

### 選項 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 或 undefined。在此範例中，`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：RemoteStageStream 層協助程式
<a name="android-layered-encoding-simulcast-remotestagestream-helpers"></a>

`RemoteStageStream` 有多個協助程式，可用來做出有關層選擇的決定，並向終端使用者顯示對應的選擇：
+ **層事件**：除了 `StageRenderer` 之外，`RemoteStageStream.Listener` 還有可傳達層和 Simulcast 調整變更的事件：
  + `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);
```