

# IVS 廣播 SDK \$1 即時串流
<a name="broadcast"></a>

Amazon Interactive Video Services (IVS) 即時串流廣播 SDK 適用於使用 Amazon IVS 建置應用程式的開發人員。此 SDK 的設計目的是利用 Amazon IVS 架構，並持續使用 Amazon IVS 的改善之處和新功能。作為原生廣播 SDK ，其設計目的是將對您的應用程式和使用者存取應用程式的裝置的效能影響降至最低。

請注意，廣播 SDK 用於傳送和接收影片；也就是說，您對主持人和觀眾使用相同的 SDK。無需單獨的播放器 SDK。

您的應用程式可以利用 Amazon IVS 廣播 SDK 的主要功能：
+ **高品質串流** — 廣播 SDK 支援高品質串流。從攝影機擷取影片並以最高 720p 的速度對其進行編碼。
+ **自動調整位元速率** — 智慧型手機使用者處於移動狀態，網路狀況可能在整個廣播過程中變更。Amazon IVS 廣播 SDK 會自動調整影片位元速率，以適應不斷變化的網路狀況。
+ **縱向和橫向支援** — 無論使用者如何手持裝置，影像都會在右側向上顯示並正確縮放。廣播 SDK 支援縱向和橫向畫布大小。當使用者的裝置旋轉方向與影片設定的方向不同時，它會自動管理長寬比。
+ **安全串流** — 使用 TLS 加密使用者的廣播，保護串流的安全。
+ **外部音訊裝置** — Amazon IVS 廣播 SDK 支援音訊插孔，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 | 兩個主要版本 (目前版本和最新的先前版本) | 
| Edge | Windows 8.1\$1 | 兩個主要版本 (目前版本和最新的先前版本) 排除邊緣舊版 | 
| 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 行動 Web 上的頻道。如果需要廣播功能，請整合 [IVS 即時串流 Android 廣播 SDK](broadcast-android.md)。

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

Web 廣播 SDK 不提供對 Webview 或類似 Web 之環境 (電視、主控台等) 的支援。如需行動裝置實作，請參閱 [Android](broadcast-android.md) 版和 [iOS](broadcast-ios.md) 版即時串流廣播 SDK 指南。

## 必要的裝置存取權
<a name="broadcast-device-access"></a>

廣播 SDK 需要存取裝置的攝影機和麥克風，包括裝置內建的攝影機和麥克風，以及透過藍牙、USB 或音訊插孔連接的攝影機和麥克風。

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

廣播 SDK 會持續改善。請參閱可用版本的 [Amazon IVS 版本備註](release-notes.md)以及已修正的問題。如果適當，請在聯絡支援部門之前，先更新您的廣播 SDK 版本，並查看是否可以解決您的問題。

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

Amazon IVS 廣播 SDK 使用[語意版本控制](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 廣播開發套件以 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 舞台程式庫，因為其在載入時會參考程式庫運作所需的瀏覽器基本概念。若要解決此問題，請依[使用 Next 和 React 的 Web 廣播示範](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.');
   }
}
```

如需詳細資訊，請參閱[許可 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();
```

### 策略
<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[];
```

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

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

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

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

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

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

`Stage` 執行個體是一個事件觸發器。使用 `stage.on()` 時，系統會將階段的狀態傳送給主持人應用程式。主持人應用程式的 UI 更新通常可以完全由事件提供支援。事件如下所示：

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

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

```
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 和 RemoteStageStream 物件上均可用。它會傳回全面的品質指標，包括網路品質、封包統計資料、位元速率資訊和影格相關指標。

這是一種非同步方法，讓您可以透過等待或鏈結承諾來擷取統計資料。當統計資料不可用時，例如串流未啟動或無法取得內部統計資料，此方法會傳回 `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) 物件。

請注意，對於採用 simulcast 技術的視訊串流，陣列包含多個統計物件（每層一個）。

**最佳實務**
+ 輪詢頻率 — 以合理的間隔（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 瀏覽器。它可傳送串流的三個轉譯層。
  + 這讓伺服器能夠根據其網路限制，選擇要傳送給其他參與者的轉譯。
  + 連同 `maxBitrate` 及/或 `maxFramerate` 值一起指定 `simulcast` 時，預期最高轉譯層會考慮設定這些值，前提條件是 `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 承載插入正在發布的階段串流。請注意，啟用 `inBandMessaging` 會增加 SDK 記憶體用量。

承載必須是 [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) 類型。承載大小必須大於 0KB，小於 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);
});
```

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

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

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

若要以發布者身分啟用 Simulcast 分層編碼，請在執行個體化時將下列組態新增至 `LocalStageStream`：

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

根據相機裝置的輸入解析度，系統會依照*串流最佳化*的[預設層、品質和影格率](real-time-streaming-optimization.md#real-time-streaming-optimization-default-layers)小節中的定義，來編碼和傳送一定數量的層。

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

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

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

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

### 選項 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` 的層物件標籤字串
+ undefined 或 null，這表示不應選取任何層，且偏好動態調整

例如，以下策略將一律讓使用者選取可用的最低品質影片層：

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

若要重設層選擇並返回動態調整，則在策略中傳回 null 或 undefined。在此範例中，`appState` 是代表可能應用程式狀態的虛擬變數。

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

### 選項 3：RemoteStageStream 層協助程式
<a name="web-layered-encoding-simulcast-remotestagestream-helpers"></a>

`RemoteStageStream` 有多個協助程式，可用來做出有關層選擇的決定，並向終端使用者顯示對應的選擇：
+ **層事件**：除了 `StageEvents` 之外，`RemoteStageStream` 物件自身還有可傳達層和 Simulcast 調整變更的事件：
  + `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 低延遲串流使用者指南》中的[在 Amazon IVS 串流上啟用多位主持人](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，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` 的代碼屬性會報告特定問題：


| 名稱 | Code | 建議的動作 | 
| --- | --- | --- | 
| TOKEN\$1MALFORMED | 1 | 建立有效權杖，然後重試將舞台執行個體化。 | 
| TOKEN\$1EXPIRED | 2 | 建立未過期的權杖，然後重試將舞台執行個體化。 | 
| TIMEOUT | 3 | 此操作逾時。如果舞台存在且權杖是有效的，則此失敗可能是網路問題。若是如此，請等待裝置的連線復原。 | 
| FAILED (失敗) | 4 | 嘗試操作時遇到嚴重狀況。檢查錯誤詳細資訊。 如果舞台存在且權杖是有效的，則此失敗可能是網路問題。若是如此，請等待裝置的連線復原。 對於與網路穩定性相關的大多數故障，此 SDK 會在內部重試長達 30 秒，然後發出 FAILED 錯誤。 | 
| CANCELED | 5 | 檢查應用程式程式碼，並確保沒有重複的 `join`、`refreshStrategy` 或 `replaceStrategy` 調用，因為這可能會導致在操作完成之前啟動和取消重複的操作。 | 
| STAGE\$1AT\$1CAPACITY | 6 | 此錯誤表示舞台或您的帳戶達到容量上限。如果舞台已達到參與者數量上限，請重新整理策略，當舞台不再處於達到容量上限狀態時，再次嘗試操作。如果您的帳戶已達到並行訂閱或並行發布者配額限制，請透過 [AWS Service Quotas 主控台](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
  }
});
```

### 訂閱
<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 廣播 SDK 中最重要方法的資訊，請參閱參考文件，網址為 [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>

我們也發布了包含偵錯符號的廣播 SDK Android 版本。如果在 IVS 廣播 SDK 中遇到當機 (即 `libbroadcastcore.so`)，您可以使用此版本來改善 Firebase Crashlytics 中偵錯報告 (堆疊追蹤) 的品質。向 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();
```

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

# 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」。

一般而言，嚴重錯誤與 Stages 伺服器的連線有關 (無法建立連線或失去連線且無法復原)。在使用新的權杖或是裝置連線恢復時，應用程式應重新建立階段並重新加入。

非嚴重錯誤通常與發布/訂閱狀態有關，且是由 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 例外狀況，包含 error code = 1000 及 fatal = true。

**動作**：建立一個有效權杖，然後重試加入。

### 權杖過期
<a name="broadcast-android-stage-join-errors-expired-token"></a>

當階段權杖過期時，就會發生此錯誤。

SDK 會從 `stage.join` 呼叫擲出一個 Java 例外狀況，包含 error code = 1001 及 fatal = true。

**動作**：建立一個新權杖，然後重試加入。

### 權杖無效或撤銷
<a name="broadcast-android-stage-join-errors-invalid-token"></a>

當階段權杖格式正確但遭 Stages 伺服器拒絕時，就會發生此錯誤。此錯誤是透過應用程式提供的階段轉譯器以非同步方式報告。

SDK 會以例外狀況呼叫 `onConnectionStateChanged`，包含 error code = 1026 及 fatal = true。

**動作**：建立一個有效權杖，然後重試加入。

### 初始加入時出現網路錯誤
<a name="broadcast-android-stage-join-errors-network-initial-join"></a>

當 SDK 無法聯絡 Stages 伺服器以建立連線時，就會發生此錯誤。此錯誤是透過應用程式提供的階段轉譯器以非同步方式報告。

SDK 會以例外狀況呼叫 `onConnectionStateChanged`，包含 error code = 1300 及 fatal = true。

**動作**：等待裝置的連線復原，然後重試加入。

### 已加入時出現網路錯誤
<a name="broadcast-android-stage-join-errors-network-already-joined"></a>

如果裝置的網路連線中斷，SDK 可能會失去與 Stage 伺服器的連線。此錯誤是透過應用程式提供的階段轉譯器以非同步方式報告。

SDK 會以例外狀況呼叫 `onConnectionStateChanged`，包含 error code = 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` 包含 error code = 1400 及 fatal = false。

**動作**：SDK 會自動重試，因此不需執行任何動作。或者，應用程式可以重新整理策略以強制執行更多次重試。在網路完全無法連線的情況下，與 Stages 的連線可能也會失敗。

# 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 廣播 SDK 中最重要方法的資訊，請參閱參考文件，網址為 [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 Package Manager 來整合廣播 SDK。(或者，您可以手動將架構新增到您的專案中)。

### 建議：整合廣播 SDK (Swift Package Manager)
<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`，方法是將其拖曳至您的應用程式目標的**一般**索引標籤的**架構、程式庫和內嵌內容**部分中。  
![\[您的應用程式目標的一般索引標籤的架構、程式庫和內嵌內容部分。\]](http://docs.aws.amazon.com/zh_tw/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()
```

`IVSStage` 類別也可連接 `IVSStageRenderer` 和 `IVSErrorDelegate`：

```
let stage = try IVSStage(token: token, strategy: self)
stage.errorDelegate = self
stage.addRenderer(self) // multiple renderers can be added
```

### 策略
<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]
```

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

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

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

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

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

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

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

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

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

```
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`。收集完成後，您將透過可以在上 `IVSStageStream` 設定的 `IVSStageStreamDelegate` 收到統計資料。若要持續收集 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` 方法，將訊息承載嵌入至其影片串流。承載大小必須大於 0KB，小於 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()`。

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

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

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

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

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

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

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

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

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

### 選項 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 或 undefined。在此範例中，`appState` 是代表主機應用程式狀態的預留位置變數。

```
func stage(_ stage: IVSStage, participant: IVSParticipantInfo, preferredLayerFor stream: IVSRemoteStageStream) -> IVSRemoteStageStreamLayer? {
    If appState.isAutoMode {
        return nil
    } else {
        return appState.layerChoice
    }
}
```

### 選項 3：RemoteStageStream 層協助程式
<a name="ios-layered-encoding-simulcast-remotestagestream-helpers"></a>

`IVSRemoteStageStream` 有多個協助程式，可用來做出有關層選擇的決定，並向終端使用者顯示對應的選擇：
+ **層事件**：除了 `IVSStageRenderer` 之外，`IVSRemoteStageStreamDelegate` 還有可傳達層和 Simulcast 調整變更的事件：
  + `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 低延遲串流使用者指南》中的[在 Amazon IVS 串流上啟用多位主持人](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` 底下的字典項目，包含一個布林值。

一般而言，嚴重錯誤與 Stages 伺服器的連線有關 (無法建立連線或失去連線且無法復原)。在使用新的權杖或是裝置連線恢復時，應用程式應重新建立階段並重新加入。

非嚴重錯誤通常與發布/訂閱狀態有關，且是由 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 例外狀況，包含 error code = 1000 及 IVSBroadcastErrorIsFatalKey = YES。

**動作**：建立一個有效權杖，然後重試加入。

### 權杖過期
<a name="broadcast-ios-stage-join-errors-expired-token"></a>

當階段權杖過期時，就會發生此錯誤。

SDK 會擲出一個 Swift 例外狀況，包含 error code = 1001 及 IVSBroadcastErrorIsFatalKey = YES。

**動作**：建立一個新權杖，然後重試加入。

### 權杖無效或撤銷
<a name="broadcast-ios-stage-join-errors-invalid-token"></a>

當階段權杖格式正確但遭 Stages 伺服器拒絕時，就會發生此錯誤。此錯誤是透過應用程式提供的階段轉譯器以非同步方式報告。

SDK 呼叫 `stage(didChange connectionState, withError error)` 包含 error code = 1026 及 IVSBroadcastErrorIsFatalKey = YES。

**動作**：建立一個有效權杖，然後重試加入。

### 初始加入時出現網路錯誤
<a name="broadcast-ios-stage-join-errors-network-initial-join"></a>

當 SDK 無法聯絡 Stages 伺服器以建立連線時，就會發生此錯誤。此錯誤是透過應用程式提供的階段轉譯器以非同步方式報告。

SDK 呼叫 `stage(didChange connectionState, withError error)` 包含 error code = 1300 及 IVSBroadcastErrorIsFatalKey = YES。

**動作**：等待裝置的連線復原，然後重試加入。

### 已加入時出現網路錯誤
<a name="broadcast-ios-stage-join-errors-network-already-joined"></a>

如果裝置的網路連線中斷，SDK 可能會失去與 Stage 伺服器的連線。此錯誤是透過應用程式提供的階段轉譯器以非同步方式報告。

SDK 呼叫 `stage(didChange connectionState, withError error)` 包含 error code = 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` 包含 error code = 1400 及 IVSBroadcastErrorIsFatalKey = NO。

**動作**：SDK 會自動重試，因此不需執行任何動作。或者，應用程式可以重新整理策略以強制執行更多次重試。在網路完全無法連線的情況下，與 Stages 的連線可能也會失敗。

# IVS 廣播 SDK：混合裝置
<a name="broadcast-mixed-devices"></a>

混合裝置是指採用多個輸入來源但產生單一輸出的音訊和視訊裝置。混合裝置是一個強大的功能，可以讓您定義和管理多個畫面 (視訊) 元素和音軌。您可以結合來自多個來源的影片和音訊，例如相機、麥克風、螢幕擷取，以及您應用程式產生的音訊和影片。您可以使用轉換功能，在串流到 IVS 的視訊周圍移動這些來源，並在串流中途新增和移除這些來源。

混合裝置可以實現不同的影像和音訊風格。若要建立混合影像裝置，請調用：

`DeviceDiscovery.createMixedImageDevice()` (在 Android 上)

`IVSDeviceDiscovery.createMixedImageDevice()` (在 iOS 上)

傳回的裝置可以像任何其他裝置一樣連接到 `BroadcastSession` (低延遲串流) 或 `Stage` (即時串流)。

## 術語
<a name="broadcast-mixed-devices-terminology"></a>

![\[IVS 廣播混合裝置術語。\]](http://docs.aws.amazon.com/zh_tw/ivs/latest/RealTimeUserGuide/images/Broadcast_SDK_Mixer_Glossary.png)



| 術語 | Description | 
| --- | --- | 
| 裝置 | 一種產生音訊或影像輸入的硬體或軟體元件。裝置範例包括麥克風、相機、藍牙耳機和虛擬裝置，例如螢幕擷取或自訂影像輸入。 | 
| 混合裝置 | `Device` 可以像任何其他 `Device` 一樣連接到 `BroadcastSession`，但需要有其他 API 允許新增 `Source` 物件。混合裝置具有合成音訊或影像的內部混合器，可產生單一輸出音訊和影像串流。 混合裝置可以實現不同的影像或音訊風格。  | 
| 混合裝置組態 | 混合裝置的組態物件。對於混合影像裝置，此組態會設定維度和影格率等屬性。對於混合音訊裝置，此組態會設定聲道數量。 | 
|  來源 | 一種容器，可定義視覺元素在畫面上的位置，以及音軌在混音中的屬性。混合裝置可以設定零個或多個來源。可以對來源進行設定，從而影響來源媒體的使用方式。上圖顯示四個影像來源： [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/zh_tw/ivs/latest/RealTimeUserGuide/broadcast-mixed-devices.html)  | 
| 來源組態 |  進入混合裝置的來源組態物件。完整的組態物件如下所述。  | 
| 轉換 | 若要將插槽移至新位置或變更其部分屬性，請使用 `MixedDevice.transitionToConfiguration()`。此方法採用： [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/zh_tw/ivs/latest/RealTimeUserGuide/broadcast-mixed-devices.html) | 

## 混合音訊裝置
<a name="broadcast-mixed-audio-device"></a>

### 組態
<a name="broadcast-mixed-audio-device-configuration"></a>

`MixedAudioDeviceConfiguration` (在 Android 上)

`IVSMixedAudioDeviceConfiguration` (在 iOS 上)


| 名稱 | 類型 | 說明 | 
| --- | --- | --- | 
| `channels` | Integer | 混音器的輸出聲道數目。有效值：1、2。1 為單聲道音訊；2 為立體聲音訊。預設：2。 | 

### 來源組態
<a name="broadcast-mixed-audio-device-source-configuration"></a>

`MixedAudioDeviceSourceConfiguration` (在 Android 上)

`IVSMixedAudioDeviceSourceConfiguration` (在 iOS 上)


| 名稱 | 類型 | 說明 | 
| --- | --- | --- | 
| `gain` | Float | 音訊增益。這是一個倍數，因此 1 以上的任何值都會增加增益；1 以下的任何值都會減少增益。有效值：0–2。預設：1。 | 

## 混合影像裝置
<a name="broadcast-mixed-image-device"></a>

### 組態
<a name="broadcast-mixed-image-device-configuration"></a>

`MixedImageDeviceConfiguration` (在 Android 上)

`IVSMixedImageDeviceConfiguration` (在 iOS 上)


| 名稱 | 類型 | 說明 | 
| --- | --- | --- | 
| `size` | Vec2 | 影片畫布的大小。 | 
| `targetFramerate` | Integer | 混合裝置的每秒目標影格數。平均而言，應該達到此值，但系統在某些情況下 (例如高 CPU 或 GPU 負載時) 可能會捨棄影格。 | 
| `transparencyEnabled` | Boolean | 這可實現在影像來源組態上使用 `alpha` 屬性進行混合。將此設定為 `true` 會增加記憶體和 CPU 耗用量。預設：`false`。 | 

### 來源組態
<a name="broadcast-mixed-image-device-source-configuration"></a>

`MixedImageDeviceSourceConfiguration` (在 Android 上)

`IVSMixedImageDeviceSourceConfiguration` (在 iOS 上)


| 名稱 | 類型 | 說明 | 
| --- | --- | --- | 
| `alpha` | Float | 插槽的 Alpha。這是與影像中的任何 Alpha 值相乘。有效值：0–1，0 表示完全透明，1 表示完全不透明。預設：1。 | 
| `aspect` | AspectMode | 插槽中呈現的任何影像的長寬比模式。有效值： [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/zh_tw/ivs/latest/RealTimeUserGuide/broadcast-mixed-devices.html) 預設：`Fit`  | 
| `fillColor` | Vec4 | 如果插槽和影像的長寬比不相符，要搭配 `aspect Fit` 使用的填色顏色。格式為 (紅色、綠色、藍色、Alpha)。有效值 (針對每個聲道)：0–1。預設：(0, 0, 0, 0)。 | 
| `position` | Vec2 | 相對於畫布左上角的插槽位置 (像素)。插槽的原點也是左上角。 | 
| `size` | Vec2 | 插槽的大小 (像素)。設定此值時，也會將 `matchCanvasSize` 設為 `false`。預設：(0, 0)；但是，因為 `matchCanvasSize` 預設為 `true`，因此插槽的呈現大小為畫布大小，而不是 (0, 0)。 | 
| `zIndex` | Float | 插槽的相對順序。`zIndex` 值較高的插槽繪製在 `zIndex` 值較低的插槽上方。 | 

## 建立和設定混合影像裝置
<a name="broadcast-mixed-image-device-creating-configuring"></a>

![\[設定用於混音的廣播工作階段。\]](http://docs.aws.amazon.com/zh_tw/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` 會將 2D 平面移動到 3D 空間，因此，當兩個平面在動畫中間的某個點相交時，會發生轉換。這可以計算出來，但它取決於開始和結束 `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) 建立的符記。
+ 如果您使用符記交換來變更驅動 server-side-composition 配置的屬性 (例如 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`。尚未進行發布的遠端參與者，若日後開始發布，其更新後的 `userId` 與 `attributes` 將透過既有的 `onParticipantJoined` / `participantDidJoin` 轉譯器函數對外提供。

### 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`，作為初始發布事件的一部分。
+ **如果參與者*已經*發布：**則該更新會立即廣播。不過，只有行動 SDK 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>

自訂圖像輸入來源讓應用程式能將自己的圖像輸入提供給廣播開發套件，而不是僅限於預設攝影機。自訂圖像來源可以是簡單的半透明浮水印或靜態的「馬上回來」場景，也可以是允許應用程式執行額外的自訂處理，像是在相機上加上美顏濾鏡。

當您使用自訂圖像輸入來源對相機進行自訂控制時 (例如，使用需要相機存取權的美顏濾鏡程式庫)，就不再由廣播開發套件負責管理相機。而是由應用程式負責正確處理相機的生命週期。請參閱官方平台文件，以了解您的應用程式應該如何管理相機。

## 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 的 Canvas 中。但是，許多程式庫 (像是美顏濾鏡程式庫) 都有提供一種方法，讓應用程式能指定外部 `Surface` 進行渲染。你可以使用這樣的方法來將此 `Surface` 傳遞到濾鏡程式庫，這允許程式庫輸出處理過的影格，以便廣播工作階段進行串流。

此 `CustomImageSource` 可以包裝在 `LocalStageStream` 中，並由 `StageStrategy` 傳回以發布到 `Stage`。

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

建立 `DeviceDiscovery` 工作階段後，建立圖像輸入來源：

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

這個方法會傳回 `IVSCustomImageSource`，這是一個允許應用程式手動提交 `CMSampleBuffers` 的圖像來源。有關支援的像素格式，請參閱 iOS 廣播開發套件參考文件；最新版本的連結位於 [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 不再負責直接管理麥克風。相較之下，音訊資料的擷取、處理，乃至於提交至自訂來源，皆由您的應用程式負責完成。

custom-audio-source 工作流程遵循下列步驟：

1. 音訊輸入 — 建立具有指定音訊格式的自訂音訊來源 (取樣率、聲道、格式)。

1. 您的處理 — 從音訊處理管道擷取或生成音訊資料。

1. 自訂音訊來源 — 使用 `appendBuffer()` 將音訊緩衝區提交至自訂來源。

1. 階段 — 透過 `StageStrategy` 包裝在 `LocalStageStream` 中，並發布至階段。

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

此方法會傳回接受原始 PCM 音訊資料的 `CustomAudioSource`。自訂音訊來源必須設定為與音訊處理管道產生的相同音訊格式。

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


| 參數 | 選項 | Description | 
| --- | --- | --- | 
| 頻道 | 1 (單聲道)、2 (立體聲) | 音訊聲道的數量。 | 
| 取樣率 | RATE\$116000、RATE\$144100、RATE\$148000 | 音訊取樣率以 Hz 為單位。建議使用 48kHz 以獲得高品質。 | 
| 格式 | 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>

您必須使用音訊來源提供的時間戳記，才能順暢播放音訊。如果您的音訊來源不提供自己的時間戳記，請建立自己的 epoch 時間戳記，並使用影格數量和影格大小手動計算每個樣本之間的持續時間。

```
// "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_tw/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 所提供的 Android `Surface`，也可以將影格轉換為點陣圖。如果使用點陣圖，則可以透過解鎖並寫入畫布，將其轉譯到自訂影像來源所提供的基礎 `Surface`。
+ **iOS**：第三方濾鏡提供者的 SDK 必須提供套用了濾鏡效果 `CMSampleBuffer` 的攝影機影格。如需有關如何在處理攝影機影像後取得 `CMSampleBuffer` 作為最終輸出的資訊，請參閱第三方濾鏡廠商 SDK 的文件。

# 搭配 IVS 廣播 SDK 使用 BytePlus
<a name="broadcast-3p-camera-filters-integrating-byteplus"></a>

本文說明如何搭配 IVS 廣播 SDK 使用 BytePlus Effects SDK。

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

### 安裝和設定 BytePlus 效果 SDK
<a name="integrating-byteplus-android-install-effects-sdk"></a>

有關如何安裝、初始化和設定 BytePlus 效果 SDK 的詳細資訊，請參閱 BytePlus [Android Access Guide](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 效果 SDK 套用濾鏡效果的攝影機影格直接轉至 IVS 廣播 SDK，請將 BytePlus 效果 SDK 的紋理輸出轉換為點陣圖。處理影像時，SDK 會調用 `onDrawFrame()` 方法。`onDrawFrame()` 方法是 Android [GLSurfaceView.Renderer](https://developer.android.com/reference/android/opengl/GLSurfaceView.Renderer) 介面的公用方法。在 BytePlus 提供的 Android 範例應用程式中，此方法會受到每個攝影機影格的呼叫，繼而輸出紋理。同時，您可以使用邏輯來補充 `onDrawFrame()` 方法，將此紋理轉換為點陣圖並將其提供給自訂影像輸入來源。如下列程式碼範例所示，請使用 BytePlus SDK 提供的 `transferTextureToBitmap` 方法來執行此轉換。這個方法由來自 BytePlus 效果 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 的 Canvas，將其轉譯到 `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);
```

# 搭配 IVS 廣播 SDK 使用 DeepAR
<a name="broadcast-3p-camera-filters-integrating-deepar"></a>

本文說明如何搭配 IVS 廣播 SDK 使用 DeepAR SDK。

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

有關如何整合 DeepAR SDK 與 Android IVS 廣播 SDK 的詳細訊息，請參閱 [Android Integration Guide from DeepAR](https://docs.deepar.ai/deepar-sdk/integrations/video-calling/amazon-ivs/android/)。

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

有關如何整合 DeepAR SDK 與 iOS IVS 廣播 SDK 的詳細訊息，請參閱 [iOS Integration Guide from DeepAR](https://docs.deepar.ai/deepar-sdk/integrations/video-calling/amazon-ivs/ios/)。

# 搭配 IVS 廣播 SDK 使用 Snap
<a name="broadcast-3p-camera-filters-integrating-snap"></a>

本文說明如何搭配 IVS 廣播 SDK 使用 Snap 的攝影機套件 SDK。

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

本節假設您已熟悉[使用 Web 廣播 SDK 發布和訂閱影片](getting-started-pub-sub-web.md)。

若要整合 Snap 的攝影機套件 SDK 與 IVS 即時串流 Web 廣播 SDK，您需要：

1. 安裝攝影機套件 SDK 和 Webpack。(我們的範例使用 Webpack 作為打包工具，但您可以自行選擇任何打包工具。)

1. 建立 `index.html`。

1. 新增設定元素。

1. 建立 `index.css`。

1. 顯示和設定參與者。

1. 顯示連接的攝影機和麥克風。

1. 建立攝影機套件工作階段。

1. 擷取鏡頭並填入鏡頭選擇器。

1. 將攝影機套件工作階段的輸出轉譯至畫布。

1. 建立函數以填入「鏡頭」下拉式清單。

1. 為攝影機套件提供用於轉譯和發布 `LocalStageStream` 的媒體來源。

1. 建立 `package.json`。

1. 建立一個 Webpack 組態檔。

1. 設定 HTTPS 伺服器和測試。

下文將介紹上述每個步驟。

### 安裝攝影機套件 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 版本取代 `<SDK version>`。

#### 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 檔案並將它們綁定到單一檔案中，以便將攝影機套件匯入為模組。綁定的 JavaScript 檔案將包含設定攝影機套件、套用鏡頭和將套用了鏡頭的攝影機供稿發布到階段的邏輯。) 新增 `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 攝影機套件 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,
    },
  });
}
```

### 建立攝影機套件工作階段
<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 和攝影機套件 Web SDK，並初始化我們將在每個 SDK 中使用的變數。我們在[引導攝影機套件 Web SDK](https://kit.snapchat.com/reference/CameraKit/web/0.7.0/index.html#bootstrapping-the-sdk) 後透過呼叫 `createSession` 建立起攝影機套件工作階段。請注意，畫布元素物件會被傳遞給工作階段；這將告知攝影機套件轉譯至該畫布。

#### 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 的預留位置取代為您自己的 ID，該 ID 可以在 [Camera Kit Developer Portal](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);
```

### 將攝影機套件工作階段的輸出轉譯到畫布
<a name="integrating-snap-web-render-output-to-canvas"></a>

使用 [captureStream](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/captureStream) 方法回傳畫布內容中的 `MediaStream`。畫布將包含套用了鏡頭的攝影機供稿的影片串流。此外，新增了用於攝影機和麥克風靜音按鈕的事件接聽程式，以及用於加入和離開階段的事件接聽程式。在用於加入階段的事件接聽程式中，我們從畫布傳遞攝影機套件工作階段和 `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]);
  }
};
```

### 為攝影機套件提供用於轉譯的媒體來源並發布 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)。接著將 `CameraKit` 媒體來源[轉換](https://docs.snap.com/camera-kit/integrate-sdk/web/web-configuration#2d-transforms)成前置攝影機的鏡像。然後，鏡頭效果被套用到媒體來源，並通過呼叫 `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 陳述式來使用攝影機套件。

#### 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'),
  },
};
```

最後，按照 Webpack 組態檔的定義執行 `npm run build` 來綁定自己的 JavaScript。如為測試用途，您可以從本機電腦提供 HTML 和 JavaScript。在此範例中，我們會使用 Python 的 `http.server` 模組。

### 設定 HTTPS 伺服器和測試
<a name="integrating-snap-web-https-server-test"></a>

若要測試程式碼，我們需要設定 HTTPS 伺服器。使用 HTTPS 伺服器進行 Web 應用程式與 Snap 攝影機套件 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 鏡頭 AR 效果。

請注意，此設定使用 Python 的內建 `http.server` 和 `ssl` 模組，適用於本機開發和測試用途，但不建議用於生產環境。此設定中使用的自我簽署 SSL/TLS 憑證不受 Web 瀏覽器和其他用戶端信任，如此表示使用者在存取伺服器時會遇到安全警告。此外，雖然我們在此範例中使用 Python 的內建 http.server 和 ssl 模組，但您可以選擇使用其他 HTTPS 伺服器解決方案。

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

若要整合 Snap 的攝影機套件 SDK 與 IVS Android 廣播 SDK，您必須安裝攝影機套件 SDK、初始化攝影機套件工作階段、套用鏡頭，然後將攝影機套件工作階段的輸出提供給自訂影像輸入來源。

要安裝攝影機套件 SDK，請將以下內容新增到模組的 `build.gradle` 檔案中。將 `$cameraKitVersion` 替換為[攝影機套件 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`。攝影機套件還為 Android 的 [CameraX](https://developer.android.com/media/camera/camerax) API 提供了一個方便的包裝函式，讓您無需編寫複雜的邏輯即可共用 CameraX 與攝影機套件。您可以使用 `CameraXImageProcessorSource` 物件作為r [ImageProcessor](https://snapchat.github.io/camera-kit-reference/api/android/latest/-camera-kit/com.snap.camerakit/-image-processor/index.html) 的 [Source](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 Developer Portal](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 ML Kit 將其分割成前景和背景組件。

1. 組合產生的分割遮罩與自訂背景影像。

1. 將其傳遞給自訂影像來源以進行廣播。

![\[實作背景替換的工作流程。\]](http://docs.aws.amazon.com/zh_tw/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 影像分割器](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 版本取代 `<SDK version>`。

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

接下來，在 body 標籤中新增一個影片元素和兩個畫布元素。影片元素會包含即時攝影機供稿，並將用作 MediaPipe 影像分割器的輸入。第一個畫布元素將用於轉譯要廣播的供稿的預覽。第二個畫布元素將用於轉譯要當作背景的自訂影像。由於具有自訂影像的第二個畫布僅用於將像素以編程方式複製到最終畫布的來源，檢視中會隱藏該畫布。

#### 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` 指派給影片物件。稍後，此影片物件將傳遞給影像分割器。另外，設定一個名為 `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>

將此組態新增到自己的 Webpack 組態檔來綁定 `app.js`，讓匯入呼叫起作用：

#### 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 ML Kit](https://developers.google.com/ml-kit/vision/selfie-segmentation) 的自拍分割 API。自拍分割 API 接受攝影機影像作為輸入，並可傳回遮罩為影像的每個像素提供信賴度分數，指出該像素是在前景中還是背景中。然後，您就能根據信賴度分數從背景影像或前景影像擷取對應的像素顏色。這個過程會持續進行，直到檢查完遮罩中的所有信賴度分數為止。結果會產生一個新的像素顏色陣列，其中包含前景像素與背景影像中像素的組合。

若要整合背景替換與 IVS 即時串流 Android 廣播 SDK，您需要：

1. 安裝 CameraX 程式庫和 Google ML Kit。

1. 初始化樣板變數。

1. 建立自訂影像來源。

1. 管理攝影機影格。

1. 將攝影機影格傳遞給 Google ML Kit。

1. 將攝影機影格前景覆疊到自訂背景上。

1. 將新影像提供給自訂影像來源。

### 安裝 CameraX 程式庫和 Google ML Kit
<a name="background-replacement-android-install-camerax-googleml"></a>

要從即時攝影機供稿中提取影像，請使用 Android 的 CameraX 程式庫。要安裝 CameraX 程式庫和 Google ML Kit，請將以下內容新增到模組的 `build.gradle` 檔案中。用最新版本的 [CameraX](https://developer.android.com/jetpack/androidx/releases/camera) 和 [Google ML Kit](https://developers.google.com/ml-kit/vision/selfie-segmentation/android) 程式庫分別替換 `${camerax_version}` 與 `${google_ml_kit_version}`。

#### 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) 中初始化一個分割器執行個體：

#### 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 ML Kit
<a name="background-replacement-android-frames-to-mlkit"></a>

接下來，建立 `InputImage` 並將其傳遞給分割器的執行個體進行處理。可在 `ImageAnalysis` 執行個體提供的 `ImageProxy` 中建立 `InputImage`。只要將 `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)
```

以下是獲取攝影機影格、傳遞給分割器並覆疊在背景上的完整函數：

#### 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 的已知問題**：使用此預設值且不連接麥克風，會導致音訊透過耳機 (而不是裝置喇叭) 播放。此預設值只能與麥克風搭配使用。


| Category | Android | iOS | 
| --- | --- | --- | 
| 回音消除 | 已啟用 | 已啟用 | 
| 音量鍵 | 通話音量 | 通話音量 | 
| 麥克風選擇 | 受作業系統限制。USB 麥克風可能無法使用。 | 受作業系統限制。USB 和藍牙麥克風可能無法使用。 同時處理輸入和輸出的藍牙耳機應能正常工作，例如 AirPods。 | 
| 音訊輸出 | 任何輸出裝置都應能正常工作。 | 受作業系統限制。有線耳機可能無法使用。 | 
| 音訊品質 | 中/低。聽起來像是在講電話，而非播放媒體。 | 中/低。聽起來像是在講電話，而非播放媒體。 | 

### 僅限訂閱
<a name="audio-modes-presets-subscribe-only"></a>

此預設值是為您訂閱其他發布參與者的計畫而設計，並非用於發布自己。它專注於音訊品質且支持所有可用的輸出裝置。


| Category | Android | iOS | 
| --- | --- | --- | 
| 回音消除 | Disabled | Disabled | 
| 音量鍵 | 媒體音量 | 媒體音量 | 
| 麥克風選擇 | 不適用。此預設值不是為發布而設計。 | 不適用。此預設值不是為發布而設計。 | 
| 音訊輸出 | 任何輸出裝置都應能正常工作。 | 任何輸出裝置都應能正常工作。 | 
| 音訊品質 | 高。任何媒體類型都應該能清晰地播放，包括音樂。 | 高。任何媒體類型都應該能清晰地播放，包括音樂。 | 

### Studio
<a name="audio-modes-presets-studio"></a>

此預設值是為了高品質的訂閱而設計，同時也保持了發布能力。它需要錄製和播放硬體才能提供回音消除功能。這裡的一個使用案例就是使用 USB 麥克風和有線耳機。SDK 將保持最高品質的音訊，同時依靠這些裝置的物理分離防止回音。


| Category | Android | iOS | 
| --- | --- | --- | 
| 回音消除 | 平台回音消除已停用，但如果 `StageAudioConfiguration.enableEchoCancellation` 為 true，軟體回音消除仍可能發生。 | Disabled | 
| 音量鍵 | 大多數情況下的媒體音量。連接藍牙麥克風時的通話音量。 | 媒體音量 | 
| 麥克風選擇 | 任何麥克風都應該可用。 | 任何麥克風都應該可用。 | 
| 音訊輸出 | 任何輸出裝置都應能正常工作。 | 任何輸出裝置都應能正常工作。 | 
| 音訊品質 | 高。雙方應該都能發送音樂並在另一側清晰地聽到。 連接藍牙耳機後，音訊品質可能會因為啟用了藍牙 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)，以及在發布時切換是否啟用 [voice processing](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 上的回音消除功能也可以使用 `echoCancellationEnabled` 方法透過 `IVSStageAudioManager` 獨立控制。此方法可以控制是否在 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 搭配使用。連線至舞台後，IVS 即時串流廣播 SDK 仍會管理音訊播放的內部 `AVAudioEngine` 執行個體，即使未使用 SDK 的麥克風也是如此。因此，提供給 `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>

避免在 `RtcEngine` 上呼叫 `setEnableSpeakerphone`，並於使用 IVS 即時串流廣播 SDK 發布時呼叫 `enableLocalAudio(false)`。當 IVS SDK 沒有在發布時，您可以再次呼叫 `enableLocalAudio(true)`。