

# IVS 브로드캐스트 SDK \$1 실시간 스트리밍
<a name="broadcast"></a>

Amazon Interactive Video Service(IVS) 실시간 스트리밍 브로드캐스트 SDK는 Amazon IVS로 애플리케이션을 구축하는 개발자를 위한 것입니다. 이 SDK는 Amazon IVS 아키텍처를 활용하도록 설계되었으며 Amazon IVS와 함께 지속적으로 개선되고 새로운 기능이 추가됩니다. 이 기본 브로드캐스트 SDK는 애플리케이션 및 사용자가 애플리케이션에 액세스하는 데 사용하는 디바이스에 미치는 성능 영향을 최소화하도록 설계되었습니다.

브로드캐스트 SDK는 비디오를 전송하고 수신하는 데 모두 사용된다는 점에 유의하시기 바랍니다. 즉, 호스트와 시청자에게 동일한 SDK를 사용하며 별도의 플레이어 SDK가 필요하지 않습니다.

애플리케이션에서는 다음과 같은 Amazon IVS Broadcast SDK의 주요 기능을 활용할 수 있습니다.
+ **고품질 스트리밍** - 브로드캐스트 SDK는 고품질 스트리밍을 지원합니다. 카메라에서 비디오를 캡처하고 최대 720p로 인코딩합니다.
+ **자동 비트 전송률 조정** - 스마트폰 사용자는 모바일을 사용하므로 브로드캐스트 전체 과정에서 네트워크 상태가 변경될 수 있습니다. Amazon IVS Broadcast SDK는 변경되는 네트워크 상태에 맞게 비디오 비트 전송률을 자동으로 조정합니다.
+ **세로 및 가로 모드 지원** - 사용자가 디바이스를 어떻게 들고 있든 이미지가 똑바로 표시되고 크기가 적절하게 조정됩니다. 브로드캐스트 SDK는 세로 및 가로 캔버스 크기를 모두 지원합니다. 사용자가 디바이스를 구성된 방향에서 벗어나 회전시키는 경우 가로 세로 비율을 자동으로 관리합니다.
+ **보안 스트리밍** - TLS를 사용하여 사용자의 브로드캐스트를 암호화하므로 스트림을 안전하게 보호할 수 있습니다.
+ **외부 오디오 디바이스** - Amazon IVS Broadcast SDK는 오디오 잭, USB 및 Bluetooth SCO 외부 마이크를 지원합니다.

## 플랫폼 요구 사항:
<a name="broadcast-platform-requirements"></a>

### 기본 플랫폼
<a name="broadcast-native-platforms"></a>


| 플랫폼 | 지원되는 버전 | 
| --- | --- | 
| Android |  9.0 이상 – 고객은 버전 6.0 이상으로 빌드할 수 있지만 실시간 스트리밍 기능을 사용할 수 없습니다.  | 
| iOS |  14 이상  | 

IVS는 최소 4개의 주요 iOS 버전과 6개의 주요 Android 버전을 지원합니다. 현재 버전 지원은 이러한 최소 한도 이상으로 확장될 수 있습니다. 메이저 버전이 더 이상 지원되지 않을 경우 최소 3개월 전에 SDK 릴리스 노트를 통해 고객에게 알립니다.

### 데스크톱 브라우저
<a name="browser-desktop"></a>


| 브라우저 | 지원되는 플랫폼 | 지원되는 버전 | 
| --- | --- | --- | 
| Chrome | Windows, macOS | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| Firefox | Windows, macOS | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| Edge | Windows 8.1 이상 | 두 가지 주요 버전(현재 및 최신 이전 버전) 엣지 레거시 제외 | 
| Safari | macOS | 두 가지 주요 버전(현재 및 최신 이전 버전) | 

### 모바일 브라우저(iOS 및 Android)
<a name="browser-mobile"></a>


| 브라우저 | 지원되는 플랫폼 | 지원되는 버전 | 
| --- | --- | --- | 
| Chrome | iOS, Android | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| Firefox | Android | 두 가지 주요 버전(현재 및 최신 이전 버전) | 
| Safari | iOS | 두 가지 주요 버전(현재 및 최신 이전 버전) | 

#### 알려진 제한 사항
<a name="browser-mobile-limitations"></a>
+ 비디오 아티팩트와 블랙 스크린을 유발하는 성능 제약으로 인해 모든 모바일 웹 브라우저에서 동시 게시자가 3명 이하인 게시/구독을 권장합니다. 게시자가 더 필요한 경우 [오디오 전용 게시 및 구독](web-publish-subscribe.md#web-publish-subscribe-concepts-strategy-updates)을 구성하세요.
+ 성능을 고려하고 충돌이 발생할 수 있으므로 Android 모바일 웹에서 스테이지를 합성하고 채널로 브로드캐스트하는 것은 권장하지 않습니다. 브로드캐스트 기능이 필요한 경우 [IVS 실시간 스트리밍 Android 브로드캐스트 SDK](broadcast-android.md)를 통합하세요.

## 웹뷰
<a name="broadcast-webviews"></a>

웹 브로드캐스트 SDK는 웹뷰 또는 웹과 유사한 환경(TV, 콘솔 등)에 대한 지원을 제공하지 않습니다. 모바일 구현에 대한 내용은 실시간 스트리밍 브로드캐스트 SDK 가이드([Android](broadcast-android.md) 및 [iOS](broadcast-ios.md))를 참조하세요.

## 필요한 디바이스 액세스
<a name="broadcast-device-access"></a>

브로드캐스트 SDK에서는 디바이스에 기본 제공되고 Bluetooth, USB 또는 오디오 잭을 통해 연결되는 디바이스의 카메라와 마이크에 모두 액세스할 수 있어야 합니다.

## 지원
<a name="broadcast-support"></a>

브로드캐스트 SDK는 지속적으로 개선됩니다. [Amazon IVS 출시 정보](release-notes.md)를 참조하여 사용 가능한 버전 및 해결된 문제를 확인하세요. 해당하는 경우 Support에 문의하기 전에 브로드캐스트 SDK 버전을 업데이트하고 문제가 해결되는지 확인합니다.

### 버저닝
<a name="broadcast-support-versioning"></a>

Amazon IVS Broadcast 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 Broadcast SDK: 웹 가이드 \$1 실시간 스트리밍
<a name="broadcast-web"></a>

IVS 실시간 스트리밍 Web Broadcast SDK는 개발자에게 웹에서 대화형 실시간 환경을 구축할 수 있는 도구를 제공합니다. 이 SDK는 Amazon IVS로 웹 애플리케이션을 구축하는 개발자를 위한 것입니다.

Web Broadcast SDK를 사용하면 참가자가 비디오를 전송하고 수신할 수 있습니다. SDK에서는 다음 작업을 지원합니다.
+ 스테이지 참가
+ 스테이지의 다른 참가자에게 미디어 게시
+ 스테이지에 있는 다른 참가자의 미디어 구독
+ 스테이지에 게시된 비디오 및 오디오 관리 및 모니터링
+ 각 피어 연결에 대한 WebRTC 통계 가져오기
+ IVS low-latency 스트리밍 Web Broadcast SDK의 모든 작업

**최신 버전의 웹 브로드캐스트 SDK:** 1.33.0([릴리스 정보](https://docs.aws.amazon.com/ivs/latest/RealTimeUserGuide/release-notes.html#mar12-26-broadcast-web-rt)) 

**참조 문서:** Amazon IVS Web Broadcast 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 Broadcast SDK](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/broadcast.html)를 참조하세요.

**참고:** 브라우저에서 게시하는 것은 추가 소프트웨어를 설치할 필요가 없으므로 최종 사용자에게 편리합니다. 그러나 브라우저 기반 게시에는 브라우저 환경의 제약 조건과 변동성이 적용됩니다. 안정성을 우선시해야 하는 경우(예: 이벤트 스트리밍의 경우) 일반적으로 비브라우저 소스(예: OBS Studio 또는 기타 전용 인코더)에서 게시하는 것이 좋습니다.이 소스는 종종 시스템 리소스에 직접 액세스할 수 있으며 브라우저 제한을 피할 수 있습니다. 브라우저가 아닌 게시 옵션에 대한 자세한 내용은 [스트림 수집](rt-stream-ingest.md) 설명서를 참조하세요.

# IVS Web Broadcast SDK 시작하기 \$1 실시간 스트리밍
<a name="broadcast-web-getting-started"></a>

이 문서에서는 IVS Real-Time Streaming Web Broadcast SDK를 시작하는 데 관련된 단계를 안내합니다.

## 가져오기
<a name="broadcast-web-getting-started-imports"></a>

실시간의 구성 요소는 루트 브로드캐스팅 모듈이 아닌 다른 네임스페이스에 있습니다.

### 스크립트 태그 사용
<a name="broadcast-web-getting-started-imports-script"></a>

Web Broadcast SDK는 JavaScript 라이브러리로 배포되며 [https://web-broadcast.live-video.net/1.33.0/amazon-ivs-web-broadcast.js](https://web-broadcast.live-video.net/1.33.0/amazon-ivs-web-broadcast.js)에서 찾을 수 있습니다.

아래 예제에 정의된 클래스와 열거형을 글로벌 객체 `IVSBroadcastClient`에서 찾을 수 있습니다.

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

### npm 사용
<a name="broadcast-web-getting-started-imports-npm"></a>

`npm` 패키지를 설치하는 방법 

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

패키지 모듈에서 클래스, 열거형 및 유형을 가져올 수도 있습니다.

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

### 서버 측 렌더링 지원
<a name="broadcast-web-getting-started-imports-server-side-rendering"></a>

Web Broadcast SDK 스테이지 라이브러리는 로드될 때 라이브러리 작동에 필요한 브라우저 프리미티브를 참조하므로 서버 측 컨텍스트에서 로드할 수 없습니다. 이를 해결하려면 [다음 및 React를 사용한 웹 브로드캐스트 데모](https://github.com/aws-samples/amazon-ivs-broadcast-web-demo/blob/main/hooks/useBroadcastSDK.js#L26-L31)에 설명된 대로 라이브러리를 동적으로 로드하세요.

## 권한 요청
<a name="broadcast-web-request-permissions"></a>

앱에서 사용자의 카메라 및 마이크에 액세스할 수 있는 권한을 요청해야 하며 HTTPS를 사용하여 제공해야 합니다. (이는 Amazon IVS에만 국한되지 않으며 카메라와 마이크에 액세스해야 하는 모든 웹 사이트에 필요합니다.)

다음은 오디오 및 비디오 장치 모두에 대한 권한을 요청하고 캡처하는 방법을 보여 주는 예제 함수입니다.

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

자세한 내용은 [Permissions API](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API) 및 [MediaDevices.getUserMedia()](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)를 참조하세요.

## 사용 가능한 장치 목록
<a name="broadcast-web-request-list-devices"></a>

캡처할 수 있는 장치를 확인하려면 브라우저의 [MediaDevices.enumerateDevices()](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices) 메서드를 쿼리하세요.

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

## 장치에서 MediaStream 검색
<a name="broadcast-web-retrieve-mediastream"></a>

사용 가능한 장치 목록을 가져온 후 원하는 수의 장치에서 스트림을 검색할 수 있습니다. 예를 들어 `getUserMedia()` 메서드를 사용하여 카메라에서 스트림을 검색할 수 있습니다.

스트림을 캡처할 장치를 지정하려면 미디어 제약 조건의 `audio` 또는 `video` 섹션에서 `deviceId`를 명시적으로 설정할 수 있습니다. 또는 `deviceId`를 생략하고 사용자가 브라우저 프롬프트에서 장치를 선택하도록 할 수 있습니다.

`width` 및 `height` 제약 조건을 사용하여 이상적인 카메라 해상도를 지정할 수도 있습니다. (이러한 제약 조건에 대한 자세한 내용은 [여기](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#properties_of_video_tracks)를 참조하세요). SDK는 최대 방송 해상도에 해당하는 너비 및 높이 제한을 자동으로 적용합니다. 하지만 SDK에 소스를 추가한 후에 소스 종횡비가 변경되지 않도록 이를 직접 적용하는 것도 좋습니다.

실시간 스트리밍의 경우 미디어가 720p 해상도로 제한되어 있는지 확인하세요. 특히 너비 및 높이에 대한 `getUserMedia` 및 `getDisplayMedia` 제약 조건 값은 함께 곱했을 때 921600(1280\$1720)을 초과해서는 안 됩니다.

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

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

# IVS Web Broadcast SDK를 사용한 게시 및 구독 \$1 실시간 스트리밍
<a name="web-publish-subscribe"></a>

이 문서에서는 IVS Real-Time Streaming Web Broadcast SDK를 사용하여 스테이지에 게시하고 구독하는 단계를 안내합니다.

## 개념
<a name="web-publish-subscribe-concepts"></a>

실시간 기능의 3가지 핵심 개념은 [스테이지](#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`가 제공됩니다.

이벤트에서 제공하는 정보가 전략의 반환 값에 영향을 미칠 것으로 예상되지는 않습니다. 예를 들어, `shouldSubscribeToParticipant`의 반환 값은 `STAGE_PARTICIPANT_PUBLISH_STATE_CHANGED`가 호출될 때 변경되지 않을 것으로 예상됩니다. 호스트 애플리케이션이 특정 참가자를 구독하려는 경우 해당 참가자의 게시 상태와 무관하게 원하는 구독 유형을 반환해야 합니다. 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
   }
})
```

또한 [StageParticiantInfo](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 객체 모두에서 사용할 수 있습니다. 네트워크 품질, 패킷 통계, 비트 전송률 정보 및 프레임 관련 지표를 포함한 포괄적인 품질 지표가 반환됩니다.

await을 통해 또는 promise 체인으로 통계를 검색할 수 있는 비동기 메서드입니다. 통계를 사용할 수 없으면 `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) 객체가 반환됩니다.

동시 방송이 있는 비디오 스트림의 경우 배열에는 여러 통계 객체(계층당 하나)가 포함됩니다.

**모범 사례**
+ 폴링 빈도 - 성능에 영향을 미치지 않도록 `requestQualityStats()`를 적절한 간격(1\$15초)으로 호출합니다.
+ 오류 처리 - 처리 전에 항상 반환된 값이 `undefined`인지 확인합니다.
+ 메모리 관리 - 스트림이 더 이상 필요하지 않은 경우 간격/제한 시간을 지웁니다.
+ 네트워크 품질 - 네트워크로 인해 발생할 수 있는 성능 저하와 관련된 사용자 피드백에 `networkQuality`를 사용합니다. 자세한 내용은 [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 기반 브라우저에서만 사용할 수 있습니다. 이 옵션을 사용하면 스트림의 3개 렌더링 계층을 전송할 수 있습니다.
  + 이를 통해 서버는 네트워킹 제한에 따라 다른 참가자에게 전송할 변환을 선택할 수 있습니다.
  + `simulcast`가 `maxBitrate` 및/또는 `maxFramerate` 값과 함께 지정되는 경우 `maxBitrate`가 내부 SDK의 두 번째로 높은 계층의 기본 `maxBitrate` 값인 900kbps 아래로 내려가지 않는 한 이러한 값을 염두에 두고 가장 높은 변환 계층이 구성될 것으로 예상됩니다.
  + `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>

Supplemental Enhancement Information(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\$130이어야 합니다. 수신 클라이언트에는 메시지 중복을 제거하는 로직이 있어야 합니다.

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

### SEI 페이로드 읽기
<a name="sei-attributes-reading-sei-payloads"></a>

구독 중인 클라이언트는 다음 예제와 같이 `inBandMessaging`을 활성화하고 `StageEvents.STAGE_STREAM_SEI_MESSAGE_RECEIVED` 이벤트를 수신하도록 구독자의 `SubscribeConfiguration`을 구성하여 SEI 페이로드가 존재하는 경우 H.264 비디오를 게시하는 게시자의 SEI 페이로드를 읽을 수 있습니다.

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

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

## 동시 방송을 사용한 계층화된 인코딩
<a name="web-publish-subscribe-layered-encoding-simulcast"></a>

동시 방송을 사용한 계층화된 인코딩은 IVS 실시간 스트리밍 특성으로, 게시자가 여러 품질의 비디오 계층을 전송하고 구독자가 해당 계층을 동적으로 또는 수동으로 변경할 수 있습니다. 이 특성은 [스트리밍 최적화](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/real-time-streaming-optimization.html) 문서에 자세히 설명되어 있습니다.

### 계층화된 인코딩 구성(게시자)
<a name="web-layered-encoding-simulcast-configure-publisher"></a>

게시자가 동시 방송을 사용하여 계층화된 인코딩을 활성화하려면 인스턴스화 시 `LocalStageStream`에 다음 구성을 추가합니다.

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

카메라 디바이스의 입력 해상도에 따라 *스트리밍 최적화*의 [기본 계층, 품질 및 프레임 속도](real-time-streaming-optimization.md#real-time-streaming-optimization-default-layers) 섹션에 정의된 대로 설정된 수의 계층이 인코딩되고 전송됩니다.

또한 필요에 따라 동시 방송 구성 내에서 개별 계층을 구성할 수 있습니다.

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

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

또는 최대 3개 계층의 자체 사용자 지정 계층 구성을 생성할 수 있습니다. 빈 배열을 제공하거나 값을 제공하지 않으면 위에 설명된 기본값이 사용됩니다. 계층은 다음과 같은 필수 속성으로 설명됩니다.
+ `height: number;`
+ `width: number;`
+ `maxBitrateKbps: number;`
+ `maxFramerate: number;`

사전 설정부터 시작해 개별 속성을 재정의하거나 완전히 새로운 구성을 생성할 수 있습니다.

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

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

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

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

개별 계층을 구성할 때 트리거할 수 있는 최대값, 제한, 오류는 SDK 참조 설명서를 참조하세요.

### 계층화된 인코딩 구성(구독자)
<a name="web-layered-encoding-simulcast-configure-subscriber"></a>

구독자는 계층화된 인코딩을 활성화하는 데 필요하지 않습니다. 게시자가 시뮬레이터 계층을 보내는 경우 기본적으로 서버는 계층 간에 동적으로 조정되어 구독자의 디바이스 및 네트워크 조건에 따라 최적의 품질을 선택합니다.

또는 게시자가 보내는 명시적 계층을 선택하려면 아래에 설명된 몇 가지 옵션이 있습니다.

### 옵션 1: 초기 계층 품질 기본 설정
<a name="web-layered-encoding-simulcast-layer-quality-preference"></a>

`subscribeConfiguration` 전략을 사용하여 구독자로 수신할 초기 계층을 선택할 수 있습니다.

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

기본적으로 구독자는 항상 가장 낮은 품질의 계층으로 먼저 전송됩니다. 이렇게 하면 가장 높은 품질의 계층으로 천천히 올라갑니다. 이렇게 하면 최종 사용자 대역폭 소비가 최적화되고 비디오에 가장 적합한 시간이 제공되므로 네트워크가 약한 사용자의 초기 비디오 정지가 줄어듭니다.

다음 옵션은 `InitialLayerPreference`에서 사용할 수 있습니다.
+ `LOWEST_QUALITY`-서버는 가장 품질이 낮은 비디오 계층을 먼저 제공합니다. 이렇게 하면 대역폭 소비와 미디어 도달 시간이 최적화됩니다. 품질은 비디오의 크기, 비트레이트 및 프레임레이트의 조합으로 정의됩니다. 예를 들어 720p 비디오는 1080p 비디오보다 품질이 낮습니다.
+ `HIGHEST_QUALITY`-서버는 최고 품질의 비디오 계층을 먼저 제공합니다. 이렇게 하면 품질이 최적화되지만 미디어에 걸리는 시간이 늘어날 수 있습니다. 품질은 비디오의 크기, 비트레이트 및 프레임레이트의 조합으로 정의됩니다. 예를 들어 1080p 비디오는 720p 비디오보다 품질이 높습니다.

**참고:** 초기 계층 기본 설정(`initialLayerPreference` 호출)을 적용하려면 이러한 업데이트가 활성 구독에 적용되지 않으므로 재구독이 필요합니다.



### 옵션 2: 스트림용 기본 계층
<a name="web-layered-encoding-simulcast-preferred-layer"></a>

스트림이 시작되면 `preferredLayerForStream ` 전략 메서드를 사용할 수 있습니다. 이 전략 메서드는 참가자와 스트림 정보를 노출합니다.

전략 메서드는 다음과 함께 반환될 수 있습니다.
+ `RemoteStageStream.getLayers` 항목이 반환하는 항목에 따른 직접적인 계층 객체입니다.
+ `StageStreamLayer.label` 항목을 기반으로 하는 계층 객체 레이블 문자열입니다.
+ 정의되지 않음 또는 null 계층을 선택하지 않아야 하며 동적 조정이 선호됨을 나타냅니다.

예를 들어 다음 전략은 항상 사용자가 사용 가능한 비디오의 최저 품질 계층을 선택하도록 합니다.

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

계층 선택을 재설정하고 동적 적응으로 돌아가려면 전략에서 null 또는 정의되지 않음을 반환합니다. 이 예제에서는 `appState` 항목이 가능한 애플리케이션 상태를 나타내는 더미 변수입니다.

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

### 옵션 3: RemoteStageStream 계층 헬퍼
<a name="web-layered-encoding-simulcast-remotestagestream-helpers"></a>

`RemoteStageStream`에는 계층 선택에 대한 결정을 내리고 최종 사용자에게 해당 선택을 표시하는 데 사용할 수 있는 여러 헬퍼가 있습니다.
+ **계층 이벤트**-`StageEvents`와 함께 `RemoteStageStream` 객체 자체에는 계층 및 동시 방송 조정 변경을 전달하는 이벤트가 있습니다.
  + `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 Broadcast SDK의 알려진 문제 및 해결 방법 \$1 실시간 스트리밍
<a name="broadcast-web-known-issues"></a>

이 문서는 Amazon IVS Real-Time Streaming Web Broadcast 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>
+ 권한 프롬프트를 거부하려면 OS 수준에서 Safari 웹 사이트 설정의 권한을 재설정해야 합니다.
+ Safari는 기본적으로 모든 장치를 Firefox나 Chrome만큼 효과적으로 감지하지는 않습니다. 예를 들어 OBS 가상 카메라는 감지되지 않습니다.

## Firefox 제한 사항
<a name="broadcast-web-firefox-limitations"></a>
+ Firefox에서 화면을 공유하려면 시스템 권한을 활성화해야 합니다. 이를 활성화한 후 사용자가 Firefox를 다시 시작해야 제대로 작동합니다. 권한이 차단된 것으로 인식되는 경우 브라우저에서 [NotFoundError](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia#exceptions) 예외(exception)가 발생합니다.
+ `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)를 참조하세요.

## 모바일 웹 제한 사항
<a name="broadcast-web-mobile-web-limitations"></a>
+ 모바일 디바이스에서는 [getDisplayMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia#browser_compatibility) 화면 공유가 지원되지 않습니다.

  **해결 방법**: 없음
+ `leave()`을 호출하지 않고 브라우저를 닫으면 참가자가 나가기까지 15초\$130초가 걸립니다.

  **해결 방법**: 사용자가 연결을 제대로 끊도록 유도하는 UI를 추가합니다.
+ 앱을 백그라운드로 전환하면 동영상 게시가 중지됩니다.

  **해결 방법**: 게시자가 일시 중지된 경우 UI 슬레이트를 표시합니다.
+ Android 디바이스에서 카메라 음소거를 해제한 후 약 5초 동안 동영상 프레임 속도가 떨어집니다.

  **해결 방법**: 없음
+ iOS 16.0의 경우 동영상 피드가 회전하면서 늘어납니다.

  **해결 방법**: 이 알려진 OS 문제를 설명하는 UI를 표시합니다.
+ 오디오 입력 디바이스를 전환하면 오디오 출력 디바이스가 자동으로 전환됩니다.

  **해결 방법**: 없음
+ 브라우저를 백그라운드로 전환하면 게시 스트림이 검은색으로 바뀌고 오디오만 생성됩니다.

  **해결 방법**: 없음 보안상의 이유 때문입니다.

# IVS Web Broadcast SDK의 오류 처리 \$1 실시간 스트리밍
<a name="broadcast-web-error-handling"></a>

이 섹션에서는 오류 조건, Web Broadcast 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>

StageError는 SDK에서 복구할 수 없는 문제가 발생하고 복구하려면 일반적으로 앱 개입 및/또는 네트워크 재연결이 필요할 때 보고됩니다.

보고된 각 `StageError`에는 코드(또는 `StageErrorCode`), 메시지(문자열) 및 범주(`StageErrorCategory`)가 있습니다. 각각은 기본 작업 범주와 관련되어 있습니다.

오류의 작업 범주는 스테이지(`JOIN_ERROR`)에 대한 연결, 스테이지로 미디어 보내기(`PUBLISH_ERROR`) 또는 스테이지에서 들어오는 미디어 스트림(`SUBSCRIBE_ERROR`)과 관련되어 있는지에 따라 결정됩니다.

`StageError`의 코드 속성에서는 다음과 같은 특정 문제를 보고합니다.


| 이름 | 코드 | 권장 조치 | 
| --- | --- | --- | 
| TOKEN\$1MALFORMED | 1 | 유효한 토큰을 생성하고 스테이지 인스턴스화를 다시 시도합니다. | 
| TOKEN\$1EXPIRED | 2 | 만료되지 않은 토큰을 생성하고 스테이지 인스턴스화를 다시 시도합니다. | 
| TIMEOUT | 3 | 작업 시간이 초과되었습니다. 스테이지가 존재하고 토큰이 유효한 경우 이 실패는 네트워크 문제일 수 있습니다. 해당 경우에는 디바이스의 연결 복구를 기다립니다. | 
| FAILED | 4 | 작업을 시도할 때 치명적인 조건이 발생했습니다. 오류 세부 정보를 확인합니다. 스테이지가 존재하고 토큰이 유효한 경우 이 실패는 네트워크 문제일 수 있습니다. 해당 경우에는 디바이스의 연결 복구를 기다립니다. 네트워크 안정성과 관련된 대부분의 실패에서 SDK는 실패 오류를 발생시키기 전에 최대 30초 동안 내부적으로 재시도합니다. | 
| 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
  }
});
```

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

구독이 실패하면 SDK가 `ERRORED`를 보고합니다. 이는 네트워크 상태 또는 구독자 수용량이 가득 찬 스테이지 때문에 발생할 수 있습니다.

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

# IVS Broadcast SDK: Android 가이드 \$1 실시간 스트리밍
<a name="broadcast-android"></a>

IVS 실시간 스트리밍 Android Broadcast SDK를 사용하면 참가자가 Android에서 비디오를 전송하고 수신할 수 있습니다.

`com.amazonaws.ivs.broadcast` 패키지는 본 문서에서 설명하는 인터페이스를 구현합니다. SDK에서는 다음 작업을 지원합니다.
+ 스테이지 참가 
+ 스테이지의 다른 참가자에게 미디어 게시
+ 스테이지에 있는 다른 참가자의 미디어 구독
+ 스테이지에 게시된 비디오 및 오디오 관리 및 모니터링
+ 각 피어 연결에 대한 WebRTC 통계 가져오기
+ IVS 지연 시간이 짧은 스트리밍 Android Broadcast 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 Broadcast 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의 [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 샘플 리포지토리를 참조하세요.

**플랫폼 요구 사항:** Android 9.0 이상

# IVS Android Broadcast SDK 시작하기 \$1 실시간 스트리밍
<a name="broadcast-android-getting-started"></a>

이 문서에서는 IVS Real-Time Streaming Android Broadcast SDK를 시작하는 데 관련된 단계를 안내합니다.

## 라이브러리 설치
<a name="broadcast-android-install"></a>

Amazon IVS Android 브로드캐스트 라이브러리를 Android 개발 환경에 추가하는 방법은 여러 가지입니다(Gradle 직접 사용, Gradle 버전 카탈로그 사용, 또는 수동으로 SDK 설치).

**Gradle 직접 사용**: 다음과 같이 모듈의 `build.gradle` 파일에 라이브러리를 추가합니다(IVS Broadcast 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 Broadcast SDK 최신 버전의 경우).

```
[versions]
ivs="1.40.0"

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

**수동으로 SDK 설치**: 다음 위치에서 최신 버전을 다운로드합니다.

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

`-stages`가 추가된 `aar`를 다운로드해야 합니다.

**스피커폰을 통한 SDK 제어도 허용**: 어떤 설치 방법을 선택하든 상관없이 SDK에서 스피커폰을 활성화 및 비활성화할 수 있도록 매니페스트에 다음과 같은 권한도 추가합니다.

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

## 디버그 기호와 함께 SDK 사용
<a name="broadcast-android-using-debug-symbols-rt"></a>

디버그 기호가 포함된 Android Broadcast SDK 버전도 게시합니다. IVS Broadcast 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 Broadcast SDK의 `libbroadcastcore.so` 라이브러리 포함)에서 디버그 정보를 자동으로 제거하려고 시도합니다. 그러나 가끔은 이 상황이 발생하지 않습니다. 따라서 `.apk` 파일이 커질 수 있으며, 디버그 기호를 제거할 수 없고 `.so` 파일을 그대로 패키징하고 있다는 Android Gradle 플러그인의 경고 메시지가 표시될 수 있습니다. 이 상황이 발생하면 다음과 같은 작업을 수행합니다.
+ Android NDK를 설치합니다. 최신 버전이 작동합니다.
+ 애플리케이션의 `build.gradle` 파일에 `ndkVersion <your_installed_ndk_version_number>`를 추가합니다. 애플리케이션 자체에 네이티브 코드가 없더라도 이 작업을 수행합니다.

자세한 내용은 이 [문제 보고서](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 Broadcast SDK를 사용한 게시 및 구독 \$1 실시간 스트리밍
<a name="android-publish-subscribe"></a>

이 문서에서는 IVS Real-Time Streaming Android Broadcast SDK를 사용하여 스테이지에 게시하고 구독하는 단계를 안내합니다.

## 개념
<a name="android-publish-subscribe-concepts"></a>

실시간 기능의 3가지 핵심 개념은 [스테이지](#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`가 제공됩니다.

렌더러에서 제공하는 정보가 전략의 반환 값에 영향을 미칠 것으로 예상되지는 않습니다. 예를 들어, `shouldSubscribeToParticipant`의 반환 값은 `onParticipantPublishStateChanged`가 호출될 때 변경되지 않을 것으로 예상됩니다. 호스트 애플리케이션이 특정 참가자를 구독하려는 경우 해당 참가자의 게시 상태와 무관하게 원하는 구독 유형을 반환해야 합니다. SDK는 원하는 전략 상태가 스테이지 상태를 기반을 정확한 시간에 실행되도록 하는 역할을 합니다.

`StageRenderer`는 스테이지 클래스에 연결될 수 있습니다.

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

게시 참가자만 `onParticipantJoined`를 트리거하고, 참가자가 게시를 중단하거나 스테이지 세션에서 나갈 때마다 `onParticipantLeft`가 트리거됩니다.

## 미디어 스트림 게시
<a name="android-publish-subscribe-publish-stream"></a>

내장 마이크 및 카메라와 같은 로컬 디바이스는 `DeviceDiscovery`를 통해 검색됩니다. 다음은 전면 카메라와 기본 마이크를 선택한 다음 SDK에 게시될 `LocalStageStreams`로 반환하는 예제입니다.

```
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를 통해 전송되기 때문에, 구독자에게 반드시 도착한다고 보장할 수 없습니다. 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\$130이어야 합니다. 수신 클라이언트에는 메시지 중복을 제거하는 로직이 있어야 합니다.

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

수신 스트림에서 임베디드 메시지를 읽는 방법은 아래 ‘Get Supplemental Enhancement Information(SEI)’을 참조하세요.

## Supplemental Enhancement Information(SEI) 가져오기
<a name="android-publish-subscribe-sei-attributes"></a>

Supplemental Enhancement Information(SEI) NAL 유닛은 비디오와 함께 프레임 정렬 메타데이터를 저장하는 데 사용됩니다. 구독 클라이언트는 게시자의 `ImageDevice`에서 나오는 `ImageDeviceFrame` 객체의 `embeddedMessages` 속성을 검사하여 H.264 비디오를 게시하는 게시자의 SEI 페이로드를 읽을 수 있습니다. 이렇게 하려면 다음 예제와 같이 게시자의 `ImageDevice` 항목을 획득한 다음 `setOnFrameCallback`에 제공된 콜백을 통해 각 프레임을 관찰합니다.

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

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

## 백그라운드에서 세션 계속하기
<a name="android-publish-subscribe-background-session"></a>

앱이 백그라운드로 전환되면 게시를 중단하거나 다른 원격 참가자의 오디오만 구독하고 싶을 수 있습니다. 이렇게 하려면 `Strategy` 구현을 업데이트하여 게시를 중단하고 `AUDIO_ONLY`를 구독합니다(또는 해당하는 경우 `NONE`).

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

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

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

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

## 동시 방송을 사용한 계층화된 인코딩
<a name="android-publish-subscribe-layered-encoding-simulcast"></a>

동시 방송을 사용한 계층화된 인코딩은 IVS 실시간 스트리밍 특성으로, 게시자가 여러 품질의 비디오 계층을 전송하고 구독자가 해당 계층을 동적으로 또는 수동으로 구성할 수 있습니다. 이 특성은 [스트리밍 최적화](real-time-streaming-optimization.md) 문서에 자세히 설명되어 있습니다.

### 계층화된 인코딩 구성(게시자)
<a name="android-layered-encoding-simulcast-configure-publisher"></a>

게시자가 동시 방송을 사용하여 계층화된 인코딩을 활성화하려면 인스턴스화 시 `LocalStageStream`에 다음 구성을 추가합니다.

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

ImageLocalStageStream cameraStream = new ImageLocalStageStream(frontCamera, config);

// Other Stage implementation code
```

비디오 구성에서 설정한 해상도에 따라 *스트리밍 최적화*의 [기본 계층, 품질 및 프레임 속도](real-time-streaming-optimization.md#real-time-streaming-optimization-default-layers) 섹션에 정의된 대로 설정된 수의 계층이 인코딩되고 전송됩니다.

또한 필요에 따라 동시 방송 구성 내에서 개별 계층을 구성할 수 있습니다.

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

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

config.simulcast.setLayers(simulcastLayers);

ImageLocalStageStream cameraStream = new ImageLocalStageStream(frontCamera, config);

// Other Stage implementation code
```

또는 최대 3개 계층의 자체 사용자 지정 계층 구성을 생성할 수 있습니다. 빈 배열을 제공하거나 값을 제공하지 않으면 위에 설명된 기본값이 사용됩니다. 계층은 다음과 같은 필수 속성 설정자로 설명됩니다.
+ `setSize: Vec2;`
+ `setMaxBitrate: integer;`
+ `setMinBitrate: integer;`
+ `setTargetFramerate: integer;`

사전 설정부터 시작해 개별 속성을 재정의하거나 완전히 새로운 구성을 생성할 수 있습니다.

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

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

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

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

config.simulcast.setLayers(simulcastLayers);

ImageLocalStageStream cameraStream = new ImageLocalStageStream(frontCamera, config);

// Other Stage implementation code
```

개별 계층을 구성할 때 트리거할 수 있는 최대값, 제한, 오류는 SDK 참조 설명서를 참조하세요.

### 계층화된 인코딩 구성(구독자)
<a name="android-layered-encoding-simulcast-configure-subscriber"></a>

구독자는 계층화된 인코딩을 활성화하는 데 필요하지 않습니다. 게시자가 시뮬레이터 계층을 보내는 경우 기본적으로 서버는 계층 간에 동적으로 조정되어 구독자의 디바이스 및 네트워크 조건에 따라 최적의 품질을 선택합니다.

또는 게시자가 보내는 명시적 계층을 선택하려면 아래에 설명된 몇 가지 옵션이 있습니다.

### 옵션 1: 초기 계층 품질 기본 설정
<a name="android-layered-encoding-simulcast-layer-quality-preference"></a>

`subscribeConfigurationForParticipant` 전략을 사용하여 구독자로 수신할 초기 계층을 선택할 수 있습니다.

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

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

    return config;
}
```

기본적으로 구독자는 항상 가장 낮은 품질의 계층으로 먼저 전송됩니다. 이렇게 하면 가장 높은 품질의 계층으로 천천히 올라갑니다. 이렇게 하면 최종 사용자 대역폭 소비가 최적화되고 비디오에 가장 적합한 시간이 제공되므로 네트워크가 약한 사용자의 초기 비디오 정지가 줄어듭니다.

다음 옵션은 `InitialLayerPreference`에서 사용할 수 있습니다.
+ `LOWEST_QUALITY`-서버는 가장 품질이 낮은 비디오 계층을 먼저 제공합니다. 이렇게 하면 대역폭 소비와 미디어 도달 시간이 최적화됩니다. 품질은 비디오의 크기, 비트레이트 및 프레임레이트의 조합으로 정의됩니다. 예를 들어 720p 비디오는 1080p 비디오보다 품질이 낮습니다.
+ `HIGHEST_QUALITY`-서버는 최고 품질의 비디오 계층을 먼저 제공합니다. 이렇게 하면 품질이 최적화되지만 미디어에 걸리는 시간이 늘어날 수 있습니다. 품질은 비디오의 크기, 비트레이트 및 프레임레이트의 조합으로 정의됩니다. 예를 들어 1080p 비디오는 720p 비디오보다 품질이 높습니다.

**참고:** 초기 계층 기본 설정(`setInitialLayerPreference` 호출)을 적용하려면 이러한 업데이트가 활성 구독에 적용되지 않으므로 재구독이 필요합니다.

### 옵션 2: 스트림용 기본 계층
<a name="android-layered-encoding-simulcast-preferred-layer"></a>

`preferredLayerForStream` 전략 방법을 사용하면 스트림이 시작된 후 계층을 선택할 수 있습니다. 이 전략 방법은 참가자와 스트림 정보를 수신하므로 참가자 별로 계층을 선택할 수 있습니다. SDK는 스트림 계층 변경, 참가자 상태 변경 또는 호스트 애플리케이션이 전략을 새로 고치는 경우와 같은 특정 이벤트에 대한 응답으로 이 메서드를 호출합니다.

전략 메서드는 다음 중 하나일 수 있는 `RemoteStageStream.Layer` 객체를 반환합니다.
+ `RemoteStageStream.getLayers`에서 반환하는 것과 같은 계층 객체입니다.
+ null, 계층을 선택해서는 안 되며 동적 조정이 선호됨을 나타냅니다.

예를 들어 다음 전략은 항상 사용자가 사용 가능한 비디오의 최저 품질 계층을 선택하도록 합니다.

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

계층 선택을 재설정하고 동적 적응으로 돌아가려면 전략에서 null 또는 정의되지 않음을 반환합니다. 이 예제에서는 `appState` 항목이 호스트 애플리케이션 상태를 나타내는 자리 표시자입니다.

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

### 옵션 3: RemoteStageStream 계층 헬퍼
<a name="android-layered-encoding-simulcast-remotestagestream-helpers"></a>

`RemoteStageStream`에는 계층 선택에 대한 결정을 내리고 최종 사용자에게 해당 선택을 표시하는 데 사용할 수 있는 여러 헬퍼가 있습니다.
+ **계층 이벤트**-`StageRenderer` 항목과 함께 `RemoteStageStream.Listener`에는 계층 및 시뮬레이터 조정 변경을 전달하는 이벤트가 있습니다.
  + `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` 객체를 만들고 네트워크 상태가 개선되면 참가를 시도합니다.

## Bluetooth 마이크 사용
<a name="android-publish-subscribe-bluetooth-microphones"></a>

Bluetooth 마이크 장치를 사용하여 게시하기 위해서는 Bluetooth SCO 연결을 시작해야 합니다.

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

# IVS Android Broadcast SDK의 알려진 문제 및 해결 방법 \$1 실시간 스트리밍
<a name="broadcast-android-known-issues"></a>

이 문서는 Amazon IVS Real-Time Streaming Android Broadcast 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 칩셋이 장착된 디바이스는 원격 참가자의 비디오를 제대로 렌더링하지 못할 수 있습니다.

  **해결 방법**: 없음
+ 일부 디바이스에서는 디바이스 OS가 SDK를 통해 선택한 것과 다른 마이크를 선택할 수 있습니다. 이는 Amazon IVS Broadcast SDK가 `VOICE_COMMUNICATION` 오디오 경로 정의 방법을 제어할 수 없기 때문이며, 오디오 경로가 디바이스 제조업체마다 다르기 때문입니다.

  **해결 방법**: 없음
+ 일부 Android 비디오 인코더는 비디오 크기를 176x176 미만으로 구성할 수 없습니다. 크기가 작으면 오류가 발생하고 스트리밍되지 않습니다.

  **해결 방법:** 비디오 크기를 176x176 미만으로 구성하지 마세요.

# IVS Android Broadcast SDK의 오류 처리 \$1 실시간 스트리밍
<a name="broadcast-android-error-handling"></a>

이 섹션에서는 오류 조건, IVS Real-Time Streaming Android Broadcast SDK가 애플리케이션에 오류를 보고하는 방법, 이러한 오류가 발생할 경우 애플리케이션이 수행해야 하는 작업에 대한 개요를 다룹니다.

## 치명적인 오류와 치명적이지 않은 오류
<a name="broadcast-android-fatal-vs-nonfatal-errors"></a>

오류 객체에는 `BroadcastException`의 “치명적” 부울 필드가 있습니다.

일반적으로 치명적인 오류는 스테이지 서버 연결과 관련이 있습니다(연결을 설정할 수 없거나 연결이 끊어져 복구할 수 없음). 애플리케이션은 스테이지를 다시 만들고 가능하면 새 토큰을 사용하거나 디바이스 연결이 복구되면 다시 참가해야 합니다.

치명적이지 않은 오류는 일반적으로 게시/구독 상태와 관련이 있으며 게시/구독 작업을 재시도하는 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`에 대한 호출에서 오류 코드 = 1000이고 fatal = true인 Java 예외를 발생시킵니다.

**조치**: 유효한 토큰을 생성한 후 다시 참가해 보세요.

### 만료된 토큰
<a name="broadcast-android-stage-join-errors-expired-token"></a>

스테이지 토큰이 만료된 경우 발생합니다.

SDK는 `stage.join`에 대한 호출에서 오류 코드 = 1001이고 fatal = true인 Java 예외를 발생시킵니다.

**조치**: 새 토큰을 생성한 후 다시 참가해 보세요.

### 유효하지 않거나 취소된 토큰
<a name="broadcast-android-stage-join-errors-invalid-token"></a>

스테이지 토큰의 형식이 잘못되진 않았지만 스테이지 서버에서 거부된 경우 발생합니다. 이는 애플리케이션에서 제공하는 스테이지 렌더러를 통해 비동기적으로 보고됩니다.

SDK는 오류 코드 = 1026이고 fatal = true인 예외로 `onConnectionStateChanged`를 호출합니다.

**조치**: 유효한 토큰을 생성한 후 다시 참가해 보세요.

### 첫 참가 시 네트워크 오류
<a name="broadcast-android-stage-join-errors-network-initial-join"></a>

SDK가 스테이지 서버에 접속하여 연결을 설정할 수 없는 경우 발생합니다. 이는 애플리케이션에서 제공하는 스테이지 렌더러를 통해 비동기적으로 보고됩니다.

SDK는 오류 코드 = 1300이고 fatal = true인 예외로 `onConnectionStateChanged`를 호출합니다.

**조치**: 디바이스 연결이 복구될 때까지 기다린 후 다시 참가해 보세요.

### 이미 참가한 경우 네트워크 오류
<a name="broadcast-android-stage-join-errors-network-already-joined"></a>

디바이스의 네트워크 연결이 끊어지면 SDK와 스테이지 서버 연결이 끊어질 수 있습니다. 이는 애플리케이션에서 제공하는 스테이지 렌더러를 통해 비동기적으로 보고됩니다.

SDK는 오류 코드 = 1300이고 fatal = true인 예외로 `onConnectionStateChanged`를 호출합니다.

**조치**: 디바이스 연결이 복구될 때까지 기다린 후 다시 참가해 보세요.

## 게시/구독 오류
<a name="broadcast-android-publish-subscribe-errors"></a>

### Initial
<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는 관련 오류 코드와 fatal = false로 `onError`를 호출합니다.

**조치**: SDK가 자동으로 재시도하므로 조치가 필요하지 않습니다. 선택적으로 애플리케이션에서 전략을 새로 고쳐 추가 재시도를 강제할 수 있습니다.

### 이미 설정된 후 실패
<a name="broadcast-android-publish-subscribe-errors-established"></a>

게시 또는 구독이 설정된 후 실패할 수 있는데, 이는 대부분 네트워크 오류로 인한 것입니다. “네트워크 오류로 인해 피어 연결이 끊어짐”의 오류 코드는 1400입니다.

이는 애플리케이션에서 제공하는 스테이지 렌더러를 통해 비동기적으로 보고됩니다.

SDK는 게시/구독 작업을 재시도합니다. 재시도 시 게시/구독 상태는 `ATTEMPTING_PUBLISH`/`ATTEMPTING_SUBSCRIBE`입니다. 재시도가 성공하면 상태가 `PUBLISHED`/`SUBSCRIBED`로 변경됩니다.

SDK는 오류 코드 = 1400이고 fatal = false인 `onError`를 호출합니다.

**조치**: SDK가 자동으로 재시도하므로 조치가 필요하지 않습니다. 선택적으로 애플리케이션에서 전략을 새로 고쳐 추가 재시도를 강제할 수 있습니다. 전체 연결이 끊어지는 경우 스테이지에 대한 연결도 실패할 가능성이 높습니다.

# IVS Broadcast SDK: iOS 가이드 \$1 실시간 스트리밍
<a name="broadcast-ios"></a>

IVS Real-Time Streaming iOS Broadcast SDK를 사용하면 참가자가 iOS에서 비디오를 전송하고 수신할 수 있습니다.

`AmazonIVSBroadcast` 모듈은 본 문서에서 설명하는 인터페이스를 구현합니다. 지원되는 작업은 다음과 같습니다.
+ 스테이지 참가 
+ 스테이지의 다른 참가자에게 미디어 게시
+ 스테이지에 있는 다른 참가자의 미디어 구독
+ 스테이지에 게시된 비디오 및 오디오 관리 및 모니터링
+ 각 피어 연결에 대한 WebRTC 통계 가져오기
+ IVS 지연 시간이 짧은 스트리밍 iOS Broadcast 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 Broadcast 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의 [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 샘플 리포지토리를 참조하세요.

**플랫폼 요구 사항:** iOS 14 이상

# IVS iOS Broadcast SDK 시작하기 \$1 실시간 스트리밍
<a name="broadcast-ios-getting-started"></a>

이 문서에서는 IVS Real-Time Streaming iOS Broadcast SDK를 시작하는 데 관련된 단계를 안내합니다.

## 라이브러리 설치
<a name="broadcast-ios-install"></a>

Swift Package Manager를 사용하여 Broadcast SDK를 통합하는 것이 좋습니다. (또는 프레임워크를 프로젝트에 수동으로 추가할 수 있습니다.)

### 권장: Broadcast 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 실시간 스트리밍 Broadcast SDK에는 iOS 저지연 스트리밍 Broadcast 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`에는 디바이스와 시뮬레이터 모두에 대한 SDK가 포함되어 있습니다.

1. 애플리케이션 대상에 대해 **일반** 탭의 **프레임워크, 라이브러리 및 포함된 콘텐츠** 섹션으로 끌어 `AmazonIVSBroadcast.xcframework`를 포함합니다.  
![\[애플리케이션 대상에 대한 일반(General) 탭의 프레임워크, 라이브러리 및 포함된 콘텐츠(Frameworks, Libraries, and Embedded Content) 섹션.\]](http://docs.aws.amazon.com/ko_kr/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>

이 단계는 선택 사항이지만 권장됩니다. Broadcast SDK를 사용하는 동안 디바이스가 절전 모드로 전환되는 것을 방지함으로써 브로드캐스트가 중단되는 것을 막습니다.

```
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 Broadcast SDK를 사용한 게시 및 구독 \$1 실시간 스트리밍
<a name="ios-publish-subscribe"></a>

이 문서에서는 IVS Real-Time Streaming iOS Broadcast SDK를 사용하여 스테이지에 게시하고 구독하는 단계를 안내합니다.

## 개념
<a name="ios-publish-subscribe-concepts"></a>

실시간 기능의 3가지 핵심 개념은 [스테이지](#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)
```

렌더러에서 제공하는 정보가 전략의 반환 값에 영향을 미칠 것으로 예상되지는 않습니다. 예를 들어, `shouldSubscribeToParticipant`의 반환 값은 `participant:didChangePublishState`가 호출될 때 변경되지 않을 것으로 예상됩니다. 호스트 애플리케이션이 특정 참가자를 구독하려는 경우 해당 참가자의 게시 상태와 무관하게 원하는 구독 유형을 반환해야 합니다. SDK는 원하는 전략 상태가 스테이지 상태를 기반을 정확한 시간에 실행되도록 하는 역할을 합니다.

게시 참가자만 `participantDidJoin`를 트리거하고, 참가자가 게시를 중단하거나 스테이지 세션에서 나갈 때마다 `participantDidLeave`가 트리거됩니다.

## 미디어 스트림 게시
<a name="ios-publish-subscribe-publish-stream"></a>

내장 마이크 및 카메라와 같은 로컬 디바이스는 `IVSDeviceDiscovery`를 통해 검색됩니다. 다음은 전면 카메라와 기본 마이크를 선택한 다음 SDK에 게시될 `IVSLocalStageStreams`로 반환하는 예제입니다.

```
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를 통해 전송되기 때문에, 구독자에게 반드시 도착한다고 보장할 수 없습니다. 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\$130이어야 합니다. 수신 클라이언트에는 메시지 중복을 제거하는 로직이 있어야 합니다.

```
try imageDevice.embedMessage(messageData, withRepeatCount: 5)

// repeatCount: 0-30, receiving clients should handle duplicates
```

### 임베디드 메시지 읽기
<a name="ios-embed-messages-read-messages"></a>

수신 스트림에서 임베디드 메시지를 읽는 방법은 아래 ‘Get Supplemental Enhancement Information(SEI)’을 참조하세요.

## Supplemental Enhancement Information(SEI) 가져오기
<a name="ios-publish-subscribe-sei-attributes"></a>

Supplemental Enhancement Information(SEI) NAL 유닛은 비디오와 함께 프레임 정렬 메타데이터를 저장하는 데 사용됩니다. 구독 클라이언트는 게시자의 `IVSImageDevice`에서 나오는 `IVSImageDeviceFrame` 객체의 `embeddedMessages` 속성을 검사하여 H.264 비디오를 게시하는 게시자의 SEI 페이로드를 읽을 수 있습니다. 이렇게 하려면 다음 예제와 같이 게시자의 `IVSImageDevice` 항목을 획득한 다음 `setOnFrameCallback`에 제공된 콜백을 통해 각 프레임을 관찰합니다.

```
// in an IVSStageRenderer’s stage:participant:didAddStreams: function, after acquiring the new IVSImageStream

let imageDevice: IVSImageDevice? = imageStream.device as? IVSImageDevice
imageDevice?.setOnFrameCallback { frame in
	for message in frame.embeddedMessages {
    		if let seiMessage = message as? IVSUserDataUnregisteredSEIMessage {
        		let seiMessageData = seiMessage.data
        		let seiMessageUUID = seiMessage.UUID

        		// interpret the message's data based on the UUID
    		}
	}
}
```

## 백그라운드에서 세션 계속하기
<a name="ios-publish-subscribe-background-session"></a>

앱이 백그라운드로 전환될 때 원격 오디오를 들으면서 스테이지에 계속 있을 수 있지만, 본인이 이미지와 오디오를 계속 전송할 수는 없습니다. `IVSStrategy` 구현을 업데이트하여 게시를 중단하고 `.audioOnly`를 구독해야 합니다(또는 해당하는 경우 `.none`).

```
func stage(_ stage: IVSStage, shouldPublishParticipant participant: IVSParticipantInfo) -> Bool {
    return false
}
func stage(_ stage: IVSStage, shouldSubscribeToParticipant participant: IVSParticipantInfo) -> IVSStageSubscribeType {
    return .audioOnly
}
```

그런 다음 `stage.refreshStrategy()`를 호출합니다.

## 동시 방송을 사용한 계층화된 인코딩
<a name="ios-publish-subscribe-layered-encoding-simulcast"></a>

동시 방송을 사용한 계층화된 인코딩은 IVS 실시간 스트리밍 특성으로, 게시자가 여러 품질의 비디오 계층을 전송하고 구독자가 해당 계층을 동적으로 또는 수동으로 구성할 수 있습니다. 이 특성은 [스트리밍 최적화](real-time-streaming-optimization.md) 문서에 자세히 설명되어 있습니다.

### 계층화된 인코딩 구성(게시자)
<a name="ios-layered-encoding-simulcast-configure-publisher"></a>

게시자가 동시 방송을 사용하여 계층화된 인코딩을 활성화하려면 인스턴스화 시 `IVSLocalStageStream`에 다음 구성을 추가합니다.

```
// Enable Simulcast
let config = IVSLocalStageStreamVideoConfiguration()
config.simulcast.enabled = true

let cameraStream = IVSLocalStageStream(device: camera, configuration: config)

// Other Stage implementation code
```

비디오 구성에서 설정한 해상도에 따라 *스트리밍 최적화*의 [기본 계층, 품질 및 프레임 속도](real-time-streaming-optimization.md#real-time-streaming-optimization-default-layers) 섹션에 정의된 대로 설정된 수의 계층이 인코딩되고 전송됩니다.

또한 필요에 따라 동시 방송 구성 내에서 개별 계층을 구성할 수 있습니다.

```
// Enable Simulcast
let config = IVSLocalStageStreamVideoConfiguration()
config.simulcast.enabled = true

let layers = [
    IVSStagePresets.simulcastLocalLayer().default720(),
    IVSStagePresets.simulcastLocalLayer().default180()
]

try config.simulcast.setLayers(layers)

let cameraStream = IVSLocalStageStream(device: camera, configuration: config)

// Other Stage implementation code
```

또는 최대 3개 계층의 자체 사용자 지정 계층 구성을 생성할 수 있습니다. 빈 배열을 제공하거나 값을 제공하지 않으면 위에 설명된 기본값이 사용됩니다. 계층은 다음과 같은 필수 속성 설정자로 설명됩니다.
+ `setSize: CGSize;`
+ `setMaxBitrate: integer;`
+ `setMinBitrate: integer;`
+ `setTargetFramerate: float;`

사전 설정부터 시작해 개별 속성을 재정의하거나 완전히 새로운 구성을 생성할 수 있습니다.

```
// Enable Simulcast
let config = IVSLocalStageStreamVideoConfiguration()
config.simulcast.enabled = true

let customHiLayer = IVSStagePresets.simulcastLocalLayer().default720()
try customHiLayer.setTargetFramerate(15)

let layers = [
    customHiLayer,
    IVSStagePresets.simulcastLocalLayer().default180()
]

try config.simulcast.setLayers(layers)

let cameraStream = IVSLocalStageStream(device: camera, configuration: config)

// Other Stage implementation code
```

개별 계층을 구성할 때 트리거할 수 있는 최대값, 제한, 오류는 SDK 참조 설명서를 참조하세요.

### 계층화된 인코딩 구성(구독자)
<a name="ios-layered-encoding-simulcast-configure-subscriber"></a>

구독자는 계층화된 인코딩을 활성화하는 데 필요하지 않습니다. 게시자가 시뮬레이터 계층을 보내는 경우 기본적으로 서버는 계층 간에 동적으로 조정되어 구독자의 디바이스 및 네트워크 조건에 따라 최적의 품질을 선택합니다.

또는 게시자가 보내는 명시적 계층을 선택하려면 아래에 설명된 몇 가지 옵션이 있습니다.

### 옵션 1: 초기 계층 품질 기본 설정
<a name="ios-layered-encoding-simulcast-layer-quality-preference"></a>

`subscribeConfigurationForParticipant` 전략을 사용하여 구독자로 수신할 초기 계층을 선택할 수 있습니다.

```
func stage(_ stage: IVSStage, subscribeConfigurationForParticipant participant: IVSParticipantInfo) -> IVSSubscribeConfiguration {
    let config = IVSSubscribeConfiguration()

    config.simulcast.initialLayerPreference = .lowestQuality

    return config
}
```

기본적으로 구독자는 항상 가장 낮은 품질의 계층으로 먼저 전송됩니다. 이렇게 하면 가장 높은 품질의 계층으로 천천히 올라갑니다. 이렇게 하면 최종 사용자 대역폭 소비가 최적화되고 비디오에 가장 적합한 시간이 제공되므로 네트워크가 약한 사용자의 초기 비디오 정지가 줄어듭니다.

다음 옵션은 `InitialLayerPreference`에서 사용할 수 있습니다.
+ `lowestQuality`-서버는 가장 품질이 낮은 비디오 계층을 먼저 제공합니다. 이렇게 하면 대역폭 소비와 미디어 도달 시간이 최적화됩니다. 품질은 비디오의 크기, 비트레이트 및 프레임레이트의 조합으로 정의됩니다. 예를 들어 720p 비디오는 1080p 비디오보다 품질이 낮습니다.
+ `highestQuality`-서버는 최고 품질의 비디오 계층을 먼저 제공합니다. 이렇게 하면 품질이 최적화되지만 미디어에 걸리는 시간이 늘어날 수 있습니다. 품질은 비디오의 크기, 비트레이트 및 프레임레이트의 조합으로 정의됩니다. 예를 들어 1080p 비디오는 720p 비디오보다 품질이 높습니다.

**참고:** 초기 계층 기본 설정(`initialLayerPreference` 호출)을 적용하려면 이러한 업데이트가 활성 구독에 적용되지 않으므로 재구독이 필요합니다.

### 옵션 2: 스트림용 기본 계층
<a name="ios-layered-encoding-simulcast-preferred-layer"></a>

`preferredLayerForStream` 전략 방법을 사용하면 스트림이 시작된 후 계층을 선택할 수 있습니다. 이 전략 방법은 참가자와 스트림 정보를 수신하므로 참가자 별로 계층을 선택할 수 있습니다. SDK는 스트림 계층 변경, 참가자 상태 변경 또는 호스트 애플리케이션이 전략을 새로 고치는 경우와 같은 특정 이벤트에 대한 응답으로 이 메서드를 호출합니다.

전략 메서드는 다음 중 하나일 수 있는 `IVSRemoteStageStreamLayer` 객체를 반환합니다.
+ `IVSRemoteStageStream.layers`에서 반환하는 것과 같은 계층 객체입니다.
+ null, 계층을 선택해서는 안 되며 동적 조정이 선호됨을 나타냅니다.

예를 들어 다음 전략은 항상 사용자가 사용 가능한 비디오의 최저 품질 계층을 선택하도록 합니다.

```
func stage(_ stage: IVSStage, participant: IVSParticipantInfo, preferredLayerFor stream: IVSRemoteStageStream) -> IVSRemoteStageStreamLayer? {
    return stream.lowestQualityLayer
}
```

계층 선택을 재설정하고 동적 적응으로 돌아가려면 전략에서 null 또는 정의되지 않음을 반환합니다. 이 예제에서는 `appState` 항목이 호스트 애플리케이션 상태를 나타내는 자리 표시자입니다.

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

### 옵션 3: RemoteStageStream 계층 헬퍼
<a name="ios-layered-encoding-simulcast-remotestagestream-helpers"></a>

`IVSRemoteStageStream`에는 계층 선택에 대한 결정을 내리고 최종 사용자에게 해당 선택을 표시하는 데 사용할 수 있는 여러 헬퍼가 있습니다.
+ **계층 이벤트**-`IVSStageRenderer` 항목과 함께 `IVSRemoteStageStreamDelegate`에는 계층 및 시뮬레이터 조정 변경을 전달하는 이벤트가 있습니다.
  + `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>

Broadcast SDK를 통해 관리되는 카메라에서는 해상도와 프레임 속도(FPS, 즉 초당 프레임)를 최적화하여 열 발생과 에너지 소비를 최소화합니다. 이 섹션에서는 호스트 애플리케이션을 사용 사례에 따라 최적화하는 데 도움이 되도록 해상도와 프레임 속도를 선택하는 방법을 설명합니다.

`IVSCamera`로 `IVSLocalStageStream`을 생성하면 카메라가 `IVSLocalStageStreamVideoConfiguration.targetFramerate`의 프레임 속도와 `IVSLocalStageStreamVideoConfiguration.size`의 해상도에 따라 최적화됩니다. `IVSLocalStageStream.setConfiguration`을 호출하면 카메라가 더 새로운 값으로 업데이트됩니다.

## 카메라 미리 보기
<a name="resolution-framerate-camera-preview"></a>

`IVSCamera`를 `IVSBroadcastSession` 또는 `IVSStage`에 연결하지 않고 미리 보기를 생성하는 경우 기본값은 해상도 1080p, 프레임 속도 60fps입니다.

## 스테이지 브로드캐스팅
<a name="resolution-framerate-broadcast-stage"></a>

`IVSBroadcastSession`을 사용하여 `IVSStage`를 브로드캐스트하면 SDK에서는 양 세션의 기준을 충족하는 해상도와 프레임 속도로 카메라 최적화를 시도합니다.

예를 들어, 브로드캐스트의 프레임 속도가 15FPS, 해상도가 1080p로 설정되어 있고 스테이지의 프레임 속도가 30FPS, 해상도가 720p인 경우 SDK에서는 카메라 구성을 프레임 속도를 30FPS, 해상도를 1080p로 선택합니다. `IVSBroadcastSession`에서는 다른 모든 프레임을 카메라에서 삭제하고, `IVSStage`에서는 1080p 이미지를 720p로 축소합니다.

호스트 애플리케이션에서 `IVSBroadcastSession`과 `IVSStage` 모두를 카메라와 함께 사용할 계획이라면 각 구성의 `targetFramerate` 속성과 `size` 속성이 일치하는 것이 좋습니다. 일치하지 않으면 비디오를 캡처하는 동안 카메라가 자체적으로 재구성되느라 비디오-샘플 전송이 잠시 지연될 수 있습니다.

동일한 값으로 설정했을 때 호스트 애플리케이션의 사용 사례가 충족되지 않는 경우, 품질이 더 높은 카메라를 먼저 생성하면 품질이 더 낮은 세션이 추가될 때 카메라가 자체적으로 재구성되지 않도록 할 수 있습니다. 예를 들어, 1080p 및 30FPS로 브로드캐스트한 다음 720p 및 30FPS로 설정된 스테이지를 조인하면 카메라가 자체적으로 재구성되지 않으며 비디오가 중단되지 않고 계속됩니다. 이는 720p가 1080p 이하이고 30FPS가 30FPS 이하이기 때문입니다.

## 임의 프레임 속도, 해상도 및 종횡비
<a name="resolution-framerate-arbitrary"></a>

대다수 카메라 하드웨어는 30FPS의 720p 또는 60FPS의 1080p와 같은 일반적인 형식을 정확히 일치시킬 수 있습니다. 그러나 모든 형식을 정확히 일치시킬 수는 없습니다. Broadcast SDK에서는 다음과 같은 규칙(우선순위 오름차순)에 따라 카메라 구성을 선택합니다.

1. 해상도의 너비와 높이는 원하는 해상도 이상이지만, 이 제약 조건 내에서의 가장 작은 값입니다.

1. 프레임 속도는 원하는 프레임 속도 이상이지만, 이 제약 조건 내에서의 가장 작은 값입니다.

1. 종횡비는 원하는 종횡비와 일치합니다.

1. 일치하는 형식이 여러 개인 경우 시야가 가장 큰 형식이 사용됩니다.

다음은 두 가지 예제입니다.
+ 호스트 애플리케이션에서 120FPS의 4k로 브로드캐스트를 시도하고 있습니다. 선택한 카메라에서는 60FPS의 4k 또는 120FPS의 1080p만 지원합니다. 프레임 속도 규칙보다 해상도 규칙의 우선순위가 높기 때문에 선택한 형식이 60FPS의 4k가 됩니다.
+ 1910x1070이라는 불규칙한 해상도가 요청됩니다. 카메라에서는 1920x1080을 사용합니다. *주의: 1921x1080과 같은 해상도를 선택하면 카메라가 사용 가능한 다음 해상도(예: 2592x1944)로 스케일 업되어 CPU 및 메모리-대역폭 페널티가 발생합니다*.

## Android는 어떤가요?
<a name="resolution-framerate-android"></a>

Android에서는 iOS처럼 해상도나 프레임 속도가 즉시 조정되지 않으므로 Android Broadcast SDK는 영향을 받지 않습니다.

# IVS iOS Broadcast SDK의 알려진 문제 및 해결 방법 \$1 실시간 스트리밍
<a name="broadcast-ios-known-issues"></a>

이 문서는 Amazon IVS Real-Time Streaming iOS Broadcast SDK를 사용할 때 발생할 수 있는 알려진 문제를 나열하고 잠재적 해결 방법을 제안합니다.
+ Bluetooth 오디오 경로를 변경하면 예기치 않은 결과가 발생할 수 있습니다. 세션 중 새로운 디바이스를 연결하면 iOS가 입력 경로를 자동으로 변경할 수도 또는 변경을 하지 못할 수도 있습니다. 또한 연결된 여러 Bluetooth 헤드셋을 동시에 선택할 수 없습니다. 이는 일반 브로드캐스트 및 스테이지 세션 모두에서 발생합니다.

  **해결 방법:** Bluetooth 헤드셋을 사용하려는 경우 브로드캐스트 또는 스테이지를 시작하기 전에 헤드셋을 연결하고 세션 전체에서 연결된 상태로 둡니다.
+ iPhone 14, iPhone 14 Plus, iPhone 14 Pro 또는 iPhone 14 Pro Max를 사용하는 참가자는 다른 참가자에게 오디오 에코 문제를 일으킬 수 있습니다.

  **해결 방법:** 영향을 받는 디바이스를 사용하는 참가자는 헤드폰을 사용하여 다른 참가자의 에코 문제를 방지할 수 있습니다.
+ 참가자가 다른 참가자가 사용 중인 토큰으로 참가하면 별도의 오류 없이 첫 번째 연결이 끊어집니다.

  **해결 방법**: 없음
+ 게시자가 게시하는 동안 간혹 구독자가 수신받는 게시 상태가 `inactive`인 경우가 발생할 수 있습니다.

  **해결 방법:** 세션에서 나간 다음 세션에 참가해 보세요. 문제가 계속되면 게시자를 위한 새 토큰을 생성하세요.
+ 참가자가 게시 또는 구독 중일 때 네트워크가 안정적인 경우에도 네트워크 문제로 인한 연결 해제를 나타내는 코드 1400과 함께 오류가 발생할 수 있습니다.

  **해결 방법:** 다시 게시하거나 구독해 보세요.
+ 오디오 왜곡 문제는 스테이지 세션 중에 간헐적으로 발생할 수 있으며, 일반적으로 호출이 장시간 지속될 때 발생합니다.

  **해결 방법:** 오디오가 왜곡된 참가자는 세션을 나간 후 다시 참가하거나 오디오 게시를 취소하고 다시 게시함으로써 문제를 해결할 수 있습니다.

# IVS iOS Broadcast SDK의 오류 처리 \$1 실시간 스트리밍
<a name="broadcast-ios-error-handling"></a>

이 섹션에서는 오류 조건, IVS Real-Time Streaming iOS Broadcast SDK가 애플리케이션에 오류를 보고하는 방법, 이러한 오류가 발생할 경우 애플리케이션이 수행해야 하는 작업에 대한 개요를 다룹니다.

## 치명적인 오류와 치명적이지 않은 오류
<a name="broadcast-ios-fatal-vs-nonfatal-errors"></a>

오류 객체에는 “치명적” 부울 필드가 있습니다. 이는 부울을 포함하는 `IVSBroadcastErrorIsFatalKey`의 딕셔너리 항목입니다.

일반적으로 치명적인 오류는 스테이지 서버 연결과 관련이 있습니다(연결을 설정할 수 없거나 연결이 끊어져 복구할 수 없음). 애플리케이션은 스테이지를 다시 만들고 가능하면 새 토큰을 사용하거나 디바이스 연결이 복구되면 다시 참가해야 합니다.

치명적이지 않은 오류는 일반적으로 게시/구독 상태와 관련이 있으며 게시/구독 작업을 재시도하는 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는 오류 코드 = 1000이고 IVSBroadcastErrorIsFatalKey = YES인 Swift 예외를 발생시킵니다.

**조치**: 유효한 토큰을 생성한 후 다시 참가해 보세요.

### 만료된 토큰
<a name="broadcast-ios-stage-join-errors-expired-token"></a>

스테이지 토큰이 만료된 경우 발생합니다.

SDK는 오류 코드 = 1001이고 IVSBroadcastErrorIsFatalKey = YES인 Swift 예외를 발생시킵니다.

**조치**: 새 토큰을 생성한 후 다시 참가해 보세요.

### 유효하지 않거나 취소된 토큰
<a name="broadcast-ios-stage-join-errors-invalid-token"></a>

스테이지 토큰의 형식이 잘못되진 않았지만 스테이지 서버에서 거부된 경우 발생합니다. 이는 애플리케이션에서 제공하는 스테이지 렌더러를 통해 비동기적으로 보고됩니다.

SDK는 오류 코드 = 1026이고 IVSBroadcastErrorIsFatalKey = YES인 `stage(didChange connectionState, withError error)`를 호출합니다.

**조치**: 유효한 토큰을 생성한 후 다시 참가해 보세요.

### 첫 참가 시 네트워크 오류
<a name="broadcast-ios-stage-join-errors-network-initial-join"></a>

SDK가 스테이지 서버에 접속하여 연결을 설정할 수 없는 경우 발생합니다. 이는 애플리케이션에서 제공하는 스테이지 렌더러를 통해 비동기적으로 보고됩니다.

SDK는 오류 코드 = 1300이고 IVSBroadcastErrorIsFatalKey = YES인 `stage(didChange connectionState, withError error)`를 호출합니다.

**조치**: 디바이스 연결이 복구될 때까지 기다린 후 다시 참가해 보세요.

### 이미 참가한 경우 네트워크 오류
<a name="broadcast-ios-stage-join-errors-network-already-joined"></a>

디바이스의 네트워크 연결이 끊어지면 SDK와 스테이지 서버 연결이 끊어질 수 있습니다. 이는 애플리케이션에서 제공하는 스테이지 렌더러를 통해 비동기적으로 보고됩니다.

SDK는 오류 코드 = 1300이고 IVSBroadcastErrorIsFatalKey 값 = YES인 `stage(didChange connectionState, withError error)`를 호출합니다.

**조치**: 디바이스 연결이 복구될 때까지 기다린 후 다시 참가해 보세요.

## 게시/구독 오류
<a name="broadcast-ios-publish-subscribe-errors"></a>

### Initial
<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에서는 적절한 오류 코드와 `IVSBroadcastErrorIsFatalKey == NO`로 `IVSErrorDelegate:didEmitError`를 호출합니다.

**조치**: SDK가 자동으로 재시도하므로 조치가 필요하지 않습니다. 선택적으로 애플리케이션에서 전략을 새로 고쳐 추가 재시도를 강제할 수 있습니다.

### 이미 설정된 후 실패
<a name="broadcast-ios-publish-subscribe-errors-established"></a>

게시 또는 구독이 설정된 후 실패할 수 있는데, 이는 대부분 네트워크 오류로 인한 것입니다. “네트워크 오류로 인해 피어 연결이 끊어짐”의 오류 코드는 1400입니다.

이는 애플리케이션에서 제공하는 스테이지 렌더러를 통해 비동기적으로 보고됩니다.

SDK는 게시/구독 작업을 재시도합니다. 재시도 시 게시/구독 상태는 `ATTEMPTING_PUBLISH`/`ATTEMPTING_SUBSCRIBE`입니다. 재시도가 성공하면 상태가 `PUBLISHED`/`SUBSCRIBED`로 변경됩니다.

SDK는 오류 코드 = 1400이고 IVSBroadcastErrorIsFatalKey = NO인 `didEmitError`를 호출합니다.

**조치**: SDK가 자동으로 재시도하므로 조치가 필요하지 않습니다. 선택적으로 애플리케이션에서 전략을 새로 고쳐 추가 재시도를 강제할 수 있습니다. 전체 연결이 끊어지는 경우 스테이지에 대한 연결도 실패할 가능성이 높습니다.

# IVS Broadcast SDK: 혼합 디바이스
<a name="broadcast-mixed-devices"></a>

혼합 디바이스는 여러 개의 입력 소스로 단일 출력을 생성하는 오디오 및 비디오 디바이스입니다. 디바이스 혼합은 여러 화면(비디오) 요소와 오디오 트랙을 정의하고 관리할 수 있는 강력한 기능입니다. 카메라, 마이크, 화면 캡처, 앱에서 생성한 오디오 및 비디오와 같은 여러 소스의 비디오와 오디오를 결합할 수 있습니다. 전환을 사용하여 IVS로 스트리밍하는 비디오에서 이러한 소스를 이동하고 스트림 중간에 소스를 추가하거나 제거할 수 있습니다.

혼합 디바이스에는 이미지 버전과 오디오 버전이 있습니다. 혼합 이미지 디바이스를 생성하려면 다음을 직접적으로 호출합니다.

Android에서 `DeviceDiscovery.createMixedImageDevice()`

iOS에서 `IVSDeviceDiscovery.createMixedImageDevice()`

반환된 디바이스는 다른 디바이스와 마찬가지로 `BroadcastSession`(저지연 스트리밍) 또는 `Stage`(실시간 스트리밍)에 연결할 수 있습니다.

## 용어
<a name="broadcast-mixed-devices-terminology"></a>

![\[IVS 브로드캐스팅 혼합 디바이스 용어입니다.\]](http://docs.aws.amazon.com/ko_kr/ivs/latest/RealTimeUserGuide/images/Broadcast_SDK_Mixer_Glossary.png)



| Term | 설명 | 
| --- | --- | 
| 장치 | 오디오 또는 이미지 입력을 생성하는 하드웨어 또는 소프트웨어 구성 요소입니다. 디바이스의 예로는 마이크, 카메라, Bluetooth 헤드셋 및 화면 캡처 또는 사용자 정의 이미지 입력과 같은 가상 디바이스가 있습니다. | 
| 혼합 디바이스 | 다른 `Device`와 마찬가지로 `BroadcastSession`에 연결할 수 있지만, `Source` 객체를 추가할 수 있는 추가 API가 있는 `Device`입니다. 혼합 디바이스에는 오디오 또는 이미지를 합성하여 단일 출력 오디오와 이미지 스트림을 생성하는 내부 믹서가 있습니다. 혼합 디바이스에는 이미지 버전 또는 오디오 버전이 있습니다.  | 
| 혼합 디바이스 구성 | 혼합 디바이스의 구성 객체입니다. 혼합 이미지 디바이스의 경우 차원과 프레임 속도 등 속성이 구성됩니다. 혼합 오디오 디바이스의 경우 채널 개수가 구성됩니다. | 
|  소스 | 화면상의 시각적 요소의 위치와 오디오 믹스에서 오디오 트랙의 속성을 정의하는 컨테이너. 혼합 디바이스는 0개 이상의 소스로 구성할 수 있습니다. 소스에는 소스의 미디어 사용 방식에 영향을 미치는 구성이 주어집니다. 위 이미지에서는 네 가지 이미지 소스를 보여줍니다. [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/ko_kr/ivs/latest/RealTimeUserGuide/broadcast-mixed-devices.html)  | 
| 소스 구성 |  혼합 디바이스에 들어가는 소스의 구성 객체입니다. 아래에 전체 구성 객체가 설명되어 있습니다.  | 
| Transition | 슬롯을 새 위치로 이동하거나 일부 속성을 변경하려면 `MixedDevice.transitionToConfiguration()`을 사용합니다. 이 메서드는 다음을 수행합니다. [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/ko_kr/ivs/latest/RealTimeUserGuide/broadcast-mixed-devices.html) | 

## 혼합 오디오 디바이스
<a name="broadcast-mixed-audio-device"></a>

### 구성
<a name="broadcast-mixed-audio-device-configuration"></a>

Android에서 `MixedAudioDeviceConfiguration`

iOS에서 `IVSMixedAudioDeviceConfiguration`


| 이름 | Type | 설명 | 
| --- | --- | --- | 
| `channels` | Integer | 오디오 믹서의 출력 채널 수 유효한 값: 1, 2. 1은 모노 오디오, 2는 스테레오 오디오입니다. 기본값: 2. | 

### 소스 구성
<a name="broadcast-mixed-audio-device-source-configuration"></a>

Android에서 `MixedAudioDeviceSourceConfiguration`

iOS에서 `IVSMixedAudioDeviceSourceConfiguration`


| 이름 | Type | 설명 | 
| --- | --- | --- | 
| `gain` | Float | 오디오 게인. 이 값은 승수이므로 1을 초과하는 모든 값은 게인을 증가시키고 1 미만의 값은 게인을 감소시킵니다. 유효한 값은 0\$12입니다. 기본값: 1. | 

## 혼합 이미지 디바이스
<a name="broadcast-mixed-image-device"></a>

### 구성
<a name="broadcast-mixed-image-device-configuration"></a>

Android에서 `MixedImageDeviceConfiguration`

iOS에서 `IVSMixedImageDeviceConfiguration`


| 이름 | Type | 설명 | 
| --- | --- | --- | 
| `size` | Vec2 | 비디오 캔버스 크기. | 
| `targetFramerate` | Integer | 혼합 디바이스의 목표 초당 프레임 수입니다. 평균적으로 이 값을 충족해야 하지만, 특정 상황(예: 높은 CPU 또는 GPU 부하)에서는 시스템이 프레임을 떨어뜨릴 수 있습니다. | 
| `transparencyEnabled` | 부울 | 그러면 이미지 소스 구성에서 `alpha` 속성을 사용하여 블렌딩할 수 있습니다. `true`로 설정하면 메모리와 CPU 소비가 증가합니다. 기본값: `false`. | 

### 소스 구성
<a name="broadcast-mixed-image-device-source-configuration"></a>

Android에서 `MixedImageDeviceSourceConfiguration`

iOS에서 `IVSMixedImageDeviceSourceConfiguration`


| 이름 | Type | 설명 | 
| --- | --- | --- | 
| `alpha` | Float | 슬롯의 알파입니다. 이미지에 알파 값이 있는 배수입니다. 유효한 값: 0\$11. 0은 완전히 투명하고, 1은 완전히 불투명합니다. 기본값: 1. | 
| `aspect` | AspectMode | 슬롯에 렌더링된 모든 이미지에 대한 종횡비 모드. 유효한 값: [\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/ko_kr/ivs/latest/RealTimeUserGuide/broadcast-mixed-devices.html) 기본값: `Fit`  | 
| `fillColor` | Vec4 | 슬롯 및 이미지 종횡비가 일치하지 않을 때 `aspect Fit`이 사용되어 색상을 채웁니다. 형식은 (빨간색, 녹색, 파란색, 알파) 입니다. 각 채널에 대해 유효한 값: 0\$11. 기본값: (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/ko_kr/ivs/latest/RealTimeUserGuide/images/Broadcast_SDK_Mixer_Configuring.png)


다음은 가이드의 시작 부분에 있는 것과 비슷한 장면을 만들 때 사용하는 화면에 표시되는 3가지 요소입니다.
+ 카메라용 왼쪽 하단 슬롯.
+ 로고 오버레이용 오른쪽 하단 슬롯.
+ 영화용 오른쪽 상단 슬롯.

캔버스의 원점은 왼쪽 상단 모서리이며 슬롯에 대해 동일합니다. 따라서 (0, 0)에 슬롯을 배치하면 전체 슬롯이 보이는 왼쪽 상단 모서리에 배치됩니다.

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

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

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

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

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

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

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

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

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

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

## 소스 제거
<a name="broadcast-mixed-devices-removing-sources"></a>

소스를 제거하려면 제거하려는 `Source` 객체로 `MixedDevice.remove`를 직접적으로 호출합니다.

## 애니메이션 전환
<a name="broadcast-mixed-devices-animations-transitions"></a>

전환 메서드는 슬롯의 구성을 새 구성으로 대체합니다. 이 대체에 지속 시간을 0보다 높은 시간(초)을 설정하여 애니메이션을 적용할 수 있습니다.

### 애니메이션할 수 있는 속성은 무엇인가요?
<a name="broadcast-mixed-devices-animations-properties"></a>

슬롯 구조의 모든 속성에 애니메이션이 적용될 수 있는 것은 아닙니다. Float 유형을 기반으로 하는 모든 속성에 애니메이션을 적용할 수 있으며, 다른 속성은 애니메이션의 시작 또는 종료 시점에 적용됩니다.


| 이름 | 애니메이션으로 만들 수 있나요? | 임팩트 포인트 | 
| --- | --- | --- | 
| `Audio.gain` | 예 | 보간 | 
| `Image.alpha` | 예 | 보간 | 
| `Image.aspect` | 아니요 | 종료 | 
| `Image.fillColor` | 예 | 보간 | 
| `Image.position` | 예 | 보간 | 
| `Image.size` | 예 | 보간 | 
| `Image.zIndex` 참고: `zIndex`는 3D 공간을 통해 2D 평면을 이동하므로 두 평면이 애니메이션 중간 지점에서 교차할 때 전환이 발생합니다. 이 값은 계산될 수 있지만 `zIndex` 값은 시작과 종료에 따라 다릅니다. 보다 원활한 전환을 위해 이 값을 `alpha`와 결합합니다.  | 예 | 알 수 없음 | 

### 간단한 예제
<a name="broadcast-mixed-devices-animations-examples"></a>

아래는 [혼합 이미지 디바이스 생성 및 구성](#broadcast-mixed-image-device-creating-configuring)의 위에서 정의된 구성을 사용하는 전체 화면 카메라 전환 예제입니다. 0.5초에 걸쳐 애니메이션이 표시됩니다.

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

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

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

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

## 브로드캐스트 미러링
<a name="broadcast-mixed-devices-mirroring"></a>


| 다음 방향으로 브로드캐스트에 연결된 이미지 디바이스를 미러링하려면 | …에 음수 값 사용 | 
| --- | --- | 
| 수평 | 슬롯의 너비 | 
| 수직 | 슬롯의 높이 | 
| 수평 및 수직 모두 | 슬롯의 너비 및 높이 모두 | 

미러링 시 슬롯을 올바른 위치에 놓으려면 위치를 같은 값으로 조정해야 합니다.

다음은 브로드캐스트를 수평 및 수직으로 미러링하는 예입니다.

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

수평 미러링:

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

수직 미러링:

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

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

수평 미러링:

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

수직 미러링:

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

참고: 이 미러링은 `ImagePreviewView`(Android) 및 `IVSImagePreviewView`(iOS)의 `setMirrored` 메서드와 다릅니다. 해당 메서드는 디바이스의 로컬 미리 보기에만 영향을 주며 브로드캐스트에는 영향을 주지 않습니다.

# IVS Broadcast SDK: 토큰 교환 \$1 실시간 스트리밍
<a name="broadcast-mobile-token-exchange"></a>

토큰 교환을 사용하면 참가자가 다시 연결할 필요 없이 모바일 Broadcast SDK 내에서 참가자 토큰 기능을 업그레이드하거나 다운그레이드하고 토큰 속성을 업데이트할 수 있습니다. 이는 참가자가 구독 전용 기능으로 시작하여 나중에 게시 기능이 필요할 수 있는 공동 호스팅과 같은 시나리오에 유용합니다.

제한:
+ 토큰 교환은 [키 페어](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/getting-started-distribute-tokens.html#getting-started-distribute-tokens-self-signed)를 사용하여 서버에서 생성된 토큰에서만 작동합니다. [CreateParticipantToken API](https://docs.aws.amazon.com/ivs/latest/RealTimeAPIReference/API_CreateParticipantToken.html)를 통해 생성된 토큰에서는 작동하지 않습니다.
+ 토큰 교환을 사용하여 서버 측 구성 레이아웃(예: featuredParticipantAttribute 및 participantOrderAttribute)을 구동하는 속성을 변경하는 경우 참가자가 다시 연결될 때까지 활성 구성의 레이아웃이 업데이트되지 않습니다.

## 토큰 교환
<a name="broadcast-mobile-token-exchange-exchanging-tokens"></a>

토큰 교환은 간단합니다. `Stage` / `IVSStage` 객체에서 `exchangeToken` API를 호출하고 새 토큰을 제공합니다. 새 토큰의 `capabilities`가 이전 토큰의 기능과 다른 경우 새 토큰의 기능이 즉시 평가됩니다. 예를 들어 이전 토큰에 `publish` 기능이 없고 새 토큰에 기능이 있는 경우 게시를 위한 스테이지 전략 함수가 호출되어 호스트 애플리케이션이 새 기능으로 즉시 게시할지 또는 기다릴지 결정할 수 있습니다. 제거된 기능에도 마찬가지입니다. 이전 토큰에 `publish` 기능이 있고 새 토큰에 기능이 없는 경우 참가자는 게시를 위한 스테이지 전략 함수를 호출하지 않고 즉시 게시를 취소합니다.

토큰을 교환할 때 이전 토큰과 새 토큰은 다음 페이로드 필드에 대해 동일한 값을 가져야 합니다.
+ `topic`
+ `resource`
+ `jti`
+ `whip_url`
+ `events_url`

이러한 필드는 변경할 수 없습니다. 변경할 수 없는 필드를 수정하는 토큰을 교환하면 SDK가 교환을 즉시 거부합니다.

다음을 포함하여 나머지 필드를 변경할 수 있습니다.
+ `attributes`
+ `capabilities`
+ `user`
+ `_id`
+ `iat`
+ `exp`

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



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

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



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

## 업데이트 수신
<a name="broadcast-mobile-token-exchange-receiving-updates"></a>

`StageRenderer`/`IVSStageRenderer`의 함수는 토큰을 교환하여 `userId` 또는 `attributes`를 업데이트하는 이미 게시된 원격 참가자에 대한 업데이트를 수신합니다. 아직 게시하지 않은 원격 참가자에게 최종적으로 게시하는 경우 기존 `onParticipantJoined` / `participantDidJoin` 렌더러 함수를 통해 업데이트된 `userId` 및 `attributes`가 노출되었습니다.

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



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

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



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

## 업데이트 가시성
<a name="broadcast-mobile-token-exchange-visibility"></a>

참가자가 토큰을 교환하여 `userId` 또는 `attributes`를 업데이트하는 경우 이러한 변경 사항의 표시 여부는 현재 게시 상태에 따라 달라집니다.
+ **참가자가 게시하지 *않는* 경우:** 업데이트가 자동으로 처리됩니다. 최종적으로 게시하는 경우 모든 SDK 초기 게시 이벤트의 일부로 업데이트된 `userId` 및 `attributes`를 수신합니다.
+ **참가자가 이미 게시 중*인* 경우:** 업데이트가 즉시 브로드캐스트됩니다. 그러나 모바일 SDK v1.37.0 이상만 알림을 받습니다. 웹 SDK, 이전 모바일 SDK 및 서버 측 구성의 참가자는 참가자가 게시를 취소하고 다시 게시할 때까지 변경 사항을 볼 수 없습니다.

이 표에서는 지원 매트릭스를 명확히 설명합니다.


| 이벤트 참가자 | 옵저버: Mobile SDK 1.37.0 이상 | 옵저버: 이전 모바일 SDK, 웹 SDK, 서버 측 구성 | 
| --- | --- | --- | 
| 게시하지 않음(이후 시작) | ✅ 보임(참가자가 참여한 이벤트를 통해 게시 시) | ✅ 보임(참가자가 참여한 이벤트를 통해 게시 시) | 
| 이미 게시 중(다시 게시하지 않음) | ✅ 보임(참가자 메타데이터 업데이트 이벤트를 통해 즉시 표시) | ❌ 보이지 않음 | 
| 이미 게시 중(게시 취소 및 다시 게시) | ✅ 보임(참가자 메타데이터 업데이트 이벤트를 통해 즉시 표시) | ⚠️ 최종적으로 보임(참가자가 참여한 이벤트를 통해 다시 게시 시) | 

# IVS 브로드캐스트 SDK: 사용자 지정 이미지 소스 \$1 실시간 스트리밍
<a name="broadcast-custom-image-sources"></a>

사용자 지정 이미지 입력 소스를 사용하면 애플리케이션이 사전 설정된 카메라로 제한되는 대신 브로드캐스트 SDK에 자체 이미지 입력을 제공할 수 있습니다. 사용자 지정 이미지 소스는 반투명 워터마크나 '잠시 기다려 주세요' 같은 정적 이미지처럼 간단한 이미지일 수도 있고, 카메라에 뷰티 필터를 추가하는 등 앱에서 추가 사용자 지정을 허용할 수도 있습니다.

카메라의 사용자 지정 제어를 위해 사용자 정의 이미지 입력 소스를 사용하는 경우(카메라 액세스가 필요한 뷰티 필터 라이브러리 사용 등) 브로드캐스트 SDK가 카메라 관리를 담당하지 않습니다. 대신 애플리케이션은 카메라의 수명 주기를 올바르게 처리합니다. 애플리케이션이 카메라를 관리하는 방법에 대한 공식 플랫폼 설명서를 참조하세요.

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

`DeviceDiscovery` 세션을 생성한 후 이미지 입력 소스를 생성합니다.

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

이 메서드는 표준 Android [Surface](https://developer.android.com/reference/android/view/Surface)에서 지원하는 이미지 소스인 `CustomImageSource`을(를) 반환합니다. 하위 클래스 `SurfaceSource`는 크기를 조정하고 회전할 수 있습니다. 또한 `ImagePreviewView`을(를) 생성하여 콘텐츠의 미리 보기를 표시할 수 있습니다.

기본 `Surface` 검색 방법:

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

이 `Surface`은(는) Camera2, OpenGL ES 및 기타 라이브러리와 같은 이미지 제작자의 출력 버퍼로 사용할 수 있습니다. 가장 간단한 사용 사례는 정적 비트맵 또는 색상을 Surface의 캔버스에 직접 그리는 것입니다. 그러나 많은 라이브러리(뷰티 필터 라이브러리 등)는 애플리케이션이 렌더링을 위해 외부 `Surface`을(를) 지정할 수 있도록 하는 메서드를 제공합니다. 이러한 메서드를 사용하여 `Surface`을(를) 필터 라이브러리에 전달하여, 라이브러리가 브로드캐스트 세션에서 스트리밍할 수 있도록 처리된 프레임을 출력할 수 있습니다.

`CustomImageSource`는 `LocalStageStream`에 래핑되고 `Stage`에 게시하기 위해 `StageStrategy`에 의해 반환될 수 있습니다.

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

`DeviceDiscovery` 세션을 생성한 후 이미지 입력 소스를 생성합니다.

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

이 메서드는 애플리케이션이 `CMSampleBuffers`을(를) 수동으로 제출하도록 허용하는 이미지 소스인 `IVSCustomImageSource`을(를) 반환합니다. 지원되는 픽셀 형식은 iOS 브로드캐스트 SDK 참조를 참조하세요. 현재 버전에 대한 최신 링크는 최신 브로드캐스트 SDK 릴리스 [Amazon IVS 릴리스 정보](release-notes.md)에 있습니다.

사용자 지정 소스에 제출된 샘플은 스테이지로 스트리밍됩니다.

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

스트리밍 비디오의 경우 콜백에서 이 메서드를 사용하세요. 예를 들어 카메라를 사용하는 경우 `AVCaptureSession`에서 새 샘플 버퍼를 받을 때마다 애플리케이션이 해당 샘플 버퍼를 사용자 정의 이미지 소스로 전달할 수 있습니다. 원하는 경우 애플리케이션은 샘플을 사용자 정의 이미지 소스에 제출하기 전에 추가 처리(뷰티 필터 등) 를 적용할 수 있습니다.

`IVSCustomImageSource`는 `IVSLocalStageStream`에 래핑되고 `Stage`에 게시하기 위해 `IVSStageStrategy`에 의해 반환될 수 있습니다.

# IVS Broadcast SDK: 사용자 지정 오디오 소스 \$1 실시간 스트리밍
<a name="broadcast-custom-audio-sources"></a>

**참고:** 이 가이드는 IVS 실시간 스트리밍 Android Broadcast SDK에만 적용됩니다. iOS 및 웹 SDK에 대한 정보는 향후 게시될 예정입니다.

사용자 지정 오디오 입력 소스를 사용하면 애플리케이션이 디바이스의 내장 마이크로 제한되지 않고 Broadcast SDK에 자체 오디오 입력을 제공할 수 있습니다. 사용자 지정 오디오 소스를 사용하면 애플리케이션이 처리된 오디오를 효과와 함께 스트리밍하거나, 여러 오디오 스트림을 혼합하거나, 타사 오디오 처리 라이브러리와 통합할 수 있습니다.

사용자 지정 오디오 입력 소스를 사용하는 경우 Broadcast SDK는 더 이상 마이크를 직접 관리할 책임이 없습니다. 대신 애플리케이션은 오디오 데이터를 캡처, 처리 및 사용자 지정 소스에 제출할 책임이 있습니다.

사용자 지정 오디오 소스 워크플로는 다음 단계를 따릅니다.

1. 오디오 입력 - 지정된 오디오 형식(샘플 속도, 채널, 형식)으로 사용자 지정 오디오 소스를 생성합니다.

1. 처리 - 오디오 처리 파이프라인에서 오디오 데이터를 캡처하거나 생성합니다.

1. 사용자 지정 오디오 소스 - `appendBuffer()`를 사용하여 사용자 지정 소스에 오디오 버퍼를 제출합니다.

1. 스테이지 - `LocalStageStream`을 래핑하고 `StageStrategy`를 통해 스테이지에 게시합니다.

1. 참가자 - 스테이지 참가자는 처리된 오디오를 실시간으로 수신합니다.

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

### 사용자 지정 오디오 소스 생성
<a name="custom-audio-sources-android-creating-a-custom-audio-source"></a>

`DeviceDiscovery` 세션을 생성한 후 오디오 입력 소스를 생성합니다.

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

이 메서드는 원시 PCM 오디오 데이터를 허용하는 `CustomAudioSource`를 반환합니다. 사용자 지정 오디오 소스는 오디오 처리 파이프라인이 생성하는 것과 동일한 오디오 형식으로 구성되어야 합니다.

#### 지원되는 오디오 형식
<a name="custom-audio-sources-android-submitting-audio-data-supportedi-audio-formats"></a>


| 파라미터 | 옵션 | 설명 | 
| --- | --- | --- | 
| 채널 | 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>

`AudioLocalStageStream`에서 `CustomAudioSource`를 래핑하고 `StageStrategy`에서 반환합니다.

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

### 전체 예제: 오디오 처리 통합
<a name="custom-audio-sources-android-complete-example"></a>

다음은 오디오 처리 SDK와의 통합을 보여주는 전체 예제입니다.

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

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

### 모범 사례
<a name="custom-audio-sources-android-best-practices"></a>

#### 오디오 형식 일관성
<a name="custom-audio-sources-android-best-practices-audio-format-consistency"></a>

제출하는 오디오 형식이 사용자 지정 소스를 생성할 때 지정된 형식과 일치하는지 확인합니다.

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

#### 버퍼 관리
<a name="custom-audio-sources-android-best-practices-buffer-managemetn"></a>

직접 `ByteBuffers`를 사용하고 재사용하여 폐영역 회수를 최소화합니다.

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

#### 타이밍 및 동기화
<a name="custom-audio-sources-android-best-practices-timing-and-synchronization"></a>

원활한 오디오 재생을 위해 오디오 소스에서 제공하는 타임스탬프를 사용해야 합니다. 오디오 소스가 자체 타임스탬프를 제공하지 않는 경우 자체 에포크 타임스탬프를 생성하고 프레임 수와 프레임 크기를 사용하여 각 샘플 간의 기간을 수동으로 계산합니다.

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

#### 오류 처리
<a name="custom-audio-sources-android-best-practices-error-handling"></a>

항상 `appendBuffer()`의 반환 값을 확인합니다.

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

# IVS 브로드캐스트 SDK: 타사 카메라 필터 \$1 실시간 스트리밍
<a name="broadcast-3p-camera-filters"></a>

이 가이드에서는 사용자가 이미 [사용자 지정 이미지](broadcast-custom-image-sources.md) 소스에 익숙하고 [IVS 실시간 스트리밍 브로드캐스트 SDK](broadcast.md)를 애플리케이션에 통합하는 데 익숙하다고 가정합니다.

라이브 스트림 크리에이터는 카메라 필터를 사용하여 얼굴 또는 배경 모양을 확대하거나 대체할 수 있습니다. 이를 통해 잠재적으로 시청자 참여도를 높이고 시청자를 끌어들이며 라이브 스트리밍 환경을 개선할 수 있습니다.

# 타사 카메라 필터 통합
<a name="broadcast-3p-camera-filters-integrating"></a>

필터 SDK의 출력을 [사용자 지정 이미지 입력 소스](broadcast-custom-image-sources.md)에 공급하여 타사 카메라 필터 SDK를 IVS 브로드캐스트 SDK와 통합할 수 있습니다. 사용자 지정 이미지 입력 소스를 사용하면 애플리케이션이 브로드캐스트 SDK에 자체 이미지 입력을 제공할 수 있습니다. 타사 필터 공급자의 SDK는 카메라의 수명 주기를 관리하여 카메라의 이미지를 처리하고, 필터 효과를 적용하며, 사용자 지정 이미지 소스에 전달할 수 있는 형식으로 출력할 수 있습니다.

![\[필터 SDK의 출력을 사용자 지정 이미지 입력 소스에 제공하여 타사 카메라 필터 SDK를 IVS 브로드캐스트 SDK와 통합합니다.\]](http://docs.aws.amazon.com/ko_kr/ivs/latest/RealTimeUserGuide/images/3P_Camera_Filters_Integrating.png)


필터 효과가 적용된 카메라 프레임을 [사용자 지정 이미지 입력 소스](broadcast-custom-image-sources.md)에 전달할 수 있는 형식으로 변환하는 기본 제공 메서드는 타사 필터 공급자의 설명서를 참조합니다. 그 프로세스는 사용되는 IVS 브로드캐스트 SDK 버전에 따라 다릅니다.
+ **웹** - 필터 공급자는 출력을 캔버스 요소로 렌더링할 수 있어야 합니다. 그런 다음 [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 Broadcast SDK와 함께 BytePlus 사용
<a name="broadcast-3p-camera-filters-integrating-byteplus"></a>

이 문서에서는 BytePlus Effects SDK를 IVS Broadcast SDK와 함께 사용하는 방법을 설명합니다.

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

### BytePlus 효과 SDK 설치 및 설정
<a name="integrating-byteplus-android-install-effects-sdk"></a>

BytePlus Effects SDK의 설치, 초기화 및 설정 방법에 대한 자세한 내용은 BytePlus [Android 액세스 가이드](https://docs.byteplus.com/en/effects/docs/android-v4101-access-guide)를 참조하세요.

### 사용자 지정 이미지 소스 설정
<a name="integrating-byteplus-android-setup-image-source"></a>

SDK를 초기화한 후 사용자 지정 이미지 입력 소스에 필터 효과를 적용한 카메라 프레임을 피드합니다. 그러려면 `DeviceDiscovery` 객체의 인스턴스를 만들고 사용자 지정 이미지 소스를 생성해야 합니다. 카메라의 사용자 지정 제어를 위해 사용자 지정 이미지 입력 소스를 사용하는 경우 브로드캐스트 SDK는 더 이상 카메라 관리를 담당하지 않습니다. 대신 애플리케이션은 카메라의 수명 주기를 올바르게 처리합니다.

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

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

### 출력을 비트맵으로 변환 및 사용자 지정 이미지 입력 소스 제공
<a name="integrating-byteplus-android-convert-to-bitmap"></a>

BytePlus Effect SDK에서 필터 효과가 적용된 카메라 프레임을 IVS 브로드캐스트 SDK로 직접 전달하려면 BytePlus Effects SDK의 텍스처 출력을 비트맵으로 변환하세요. 이미지가 처리되면 SDK에서 `onDrawFrame()` 메서드를 호출합니다. `onDrawFrame()` 메서드는 Android의 [GLSurfaceView.Renderer](https://developer.android.com/reference/android/opengl/GLSurfaceView.Renderer) 인터페이스의 공개 메서드입니다. BytePlus에서 제공하는 Android 샘플 앱에서 이 메서드가 모든 카메라 프레임에서 호출되어 텍스처를 출력합니다. 동시에 이 텍스처를 비트맵으로 변환하고 사용자 지정 이미지 입력 소스에 공급하는 로직으로 `onDrawFrame()` 메서드를 보완할 수 있습니다. 다음 코드 샘플에서 볼 수 있듯이 BytePlus SDK에서 제공하는 `transferTextureToBitmap` 메서드를 사용하여 이 변환을 수행하세요. 이 메서드는 다음 코드 샘플과 같이 BytePlus Effects SDK의 [com.bytedance.labcv.core.util.ImageUtil](https://docs.byteplus.com/en/effects/docs/android-v4101-access-guide#Appendix:%20convert%20input%20texture%20to%202D%20texture%20with%20upright%20face) 라이브러리가 제공합니다. 그런 다음 결과 비트맵을 Surface 캔버스에 기록하여 `CustomImageSource`의 기본 Android `Surface`로 렌더링할 수 있습니다. `onDrawFrame()`을 여러 번 연속해서 호출하면 비트맵 시퀀스를 생성하고, 결합하면 비디오 스트림을 생성합니다.

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

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


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

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

# IVS Broadcast SDK와 함께 DeepAR 사용
<a name="broadcast-3p-camera-filters-integrating-deepar"></a>

이 문서에서는 DeepAR SDK를 IVS broadcast SDK와 함께 사용하는 방법을 설명합니다.

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

DeepAR SDK를 Android IVS 브로드캐스트 SDK와 통합하는 방법에 대한 자세한 내용은 [DeepAR의 Android 통합 가이드](https://docs.deepar.ai/deepar-sdk/integrations/video-calling/amazon-ivs/android/)를 참조하세요.

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

DeepAR SDK를 iOS IVS 브로드캐스트 SDK와 통합하는 방법에 대한 자세한 내용은 [DeepAR의 iOS 통합 가이드](https://docs.deepar.ai/deepar-sdk/integrations/video-calling/amazon-ivs/ios/)를 참조하세요.

# IVS Broadcast SDK와 함께 Snap 사용
<a name="broadcast-3p-camera-filters-integrating-snap"></a>

이 문서에서는 Snap의 Camera Kit SDK를 IVS broadcast SDK와 함께 사용하는 방법을 설명합니다.

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

이 섹션에서는 [웹 브로드캐스트 SDK를 사용하여 비디오를 게시 및 구독](getting-started-pub-sub-web.md)하는 방법을 이미 잘 알고 있다고 가정합니다.

Snap의 Camera Kit SDK를 IVS 실시간 스트리밍 웹 브로드캐스트 SDK와 통합하려면 다음이 필요합니다.

1. Camera Kit SDK 및 Webpack을 설치합니다. (이 예에서는 Webpack을 번들러로 사용하지만 원하는 번들러를 사용할 수 있습니다.)

1. `index.html`을 생성합니다.

1. 설정 요소를 추가하세요.

1. `index.css` 생성.

1. 참가자를 표시하고 설정하세요.

1. 연결된 카메라와 마이크를 표시하세요.

1. Camera Kit 세션을 생성하세요.

1. 렌즈를 가져오고 렌즈 선택기를 채웁니다.

1. Camera Kit 세션에서 캔버스로 출력을 렌더링하세요.

1. 렌드 드롭다운을 채우는 함수를 생성합니다.

1. Camera Kit에 렌더링용 미디어 소스를 제공하고 `LocalStageStream`을 게시하세요.

1. `package.json` 생성.

1. Webpack 구성 파일을 생성합니다.

1. HTTPS 서버를 설정하고 테스트합니다.

이러한 각 단계는 아래에서 설명하고 있습니다.

### Camera Kit SDK 및 Webpack 설치
<a name="integrating-snap-web-install-camera-kit"></a>

이 예제에서는 Webpack을 번들러로 사용하지만 모든 번들러를 사용할 수 있습니다.

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

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

다음으로 HTML 표준 문안을 생성하고 웹 브로드캐스트 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 파일을 비롯한 추가 로직을 로드합니다. (이 섹션의 뒷부분에서는 Camera Kit를 모듈로 가져올 수 있도록 이러한 JavaScript 파일을 만들고 단일 파일로 번들링합니다. 번들 JavaScript 파일에는 Camera Kit를 설정하고, Lens를 적용하고, 스테이지에 Lens를 적용한 상태로 카메라 피드를 게시하기 위한 로직이 포함됩니다.) `body` 및 `html` 요소에 대한 종료 태그를 추가하여 `index.html` 생성을 완료합니다.

#### HTML
<a name="integrating-snap-web-load-all-scripts-code"></a>

```
<!-- Load all Desired Scripts -->
  <script src="./helpers.js"></script>
  <script src="./media-devices.js"></script>
  <!-- <script type="module" src="./stages-simple.js"></script> -->
  <script src="./dist/bundle.js"></script>
</body>
</html>
```

### index.css 생성
<a name="integrating-snap-web-create-index-css"></a>

CSS 소스 파일을 생성하여 페이지를 스타일링합니다. 스테이지를 관리하고 Snap의 Camera Kit SDK와 통합하기 위한 로직에 초점을 맞추기 위해 이 코드를 검토하지 않을 것입니다.

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

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

html,
body {
  margin: 2rem;
  box-sizing: border-box;
  height: 100vh;
  max-height: 100vh;
  display: flex;
  flex-direction: column;
}

hr {
  margin: 1rem 0;
}

table {
  display: table;
}

canvas {
  margin-bottom: 1rem;
  background: green;
}

video {
  margin-bottom: 1rem;
  background: black;
  max-width: 100%;
  max-height: 150px;
}

.log {
  flex: none;
  height: 300px;
}

.content {
  flex: 1 0 auto;
}

.button {
  display: block;
  margin: 0 auto;
}

.local-container {
  position: relative;
}

.static-controls {
  position: absolute;
  margin-left: auto;
  margin-right: auto;
  left: 0;
  right: 0;
  bottom: -4rem;
  text-align: center;
}

.static-controls button {
  display: inline-block;
}

.hidden {
  display: none;
}

.participant-container {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  margin: 1rem;
}

video {
  border: 0.5rem solid #555;
  border-radius: 0.5rem;
}
.placeholder {
  background-color: #333333;
  display: flex;
  text-align: center;
  margin-bottom: 1rem;
}
.placeholder span {
  margin: auto;
  color: white;
}
#local-media {
  display: inline-block;
  width: 100vw;
}

#local-media video {
  max-height: 300px;
}

#remote-media {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: row;
  width: 100%;
}

#lens-selector {
  width: 100%;
  margin-bottom: 1rem;
}
```

### 참가자 표시 및 설정
<a name="integrating-snap-web-setup-participants"></a>

다음으로, 참가자를 표시하고 설정하는 데 사용할 도우미 메서드를 포함한 `helpers.js`를 생성하세요.

#### JavaScript
<a name="integrating-snap-web-setup-participants-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

function setupParticipant({ isLocal, id }) {
  const groupId = isLocal ? 'local-media' : 'remote-media';
  const groupContainer = document.getElementById(groupId);

  const participantContainerId = isLocal ? 'local' : id;
  const participantContainer = createContainer(participantContainerId);
  const videoEl = createVideoEl(participantContainerId);

  participantContainer.appendChild(videoEl);
  groupContainer.appendChild(participantContainer);

  return videoEl;
}

function teardownParticipant({ isLocal, id }) {
  const groupId = isLocal ? 'local-media' : 'remote-media';
  const groupContainer = document.getElementById(groupId);
  const participantContainerId = isLocal ? 'local' : id;

  const participantDiv = document.getElementById(
    participantContainerId + '-container'
  );
  if (!participantDiv) {
    return;
  }
  groupContainer.removeChild(participantDiv);
}

function createVideoEl(id) {
  const videoEl = document.createElement('video');
  videoEl.id = id;
  videoEl.autoplay = true;
  videoEl.playsInline = true;
  videoEl.srcObject = new MediaStream();
  return videoEl;
}

function createContainer(id) {
  const participantContainer = document.createElement('div');
  participantContainer.classList = 'participant-container';
  participantContainer.id = id + '-container';

  return participantContainer;
}
```

### 연결된 카메라 및 마이크 표시
<a name="integrating-snap-web-display-cameras-microphones"></a>

다음으로, 디바이스에 연결된 카메라와 마이크를 표시하는 도우미 메서드를 포함함 `media-devices.js`를 생성하세요.

#### JavaScript
<a name="integrating-snap-web-display-cameras-microphones-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

/**
 * Returns an initial list of devices populated on the page selects
 */
async function initializeDeviceSelect() {
  const videoSelectEl = document.getElementById('video-devices');
  videoSelectEl.disabled = false;

  const { videoDevices, audioDevices } = await getDevices();
  videoDevices.forEach((device, index) => {
    videoSelectEl.options[index] = new Option(device.label, device.deviceId);
  });

  const audioSelectEl = document.getElementById('audio-devices');

  audioSelectEl.disabled = false;
  audioDevices.forEach((device, index) => {
    audioSelectEl.options[index] = new Option(device.label, device.deviceId);
  });
}

/**
 * Returns all devices available on the current device
 */
async function getDevices() {
  // Prevents issues on Safari/FF so devices are not blank
  await navigator.mediaDevices.getUserMedia({ video: true, audio: true });

  const devices = await navigator.mediaDevices.enumerateDevices();
  // Get all video devices
  const videoDevices = devices.filter((d) => d.kind === 'videoinput');
  if (!videoDevices.length) {
    console.error('No video devices found.');
  }

  // Get all audio devices
  const audioDevices = devices.filter((d) => d.kind === 'audioinput');
  if (!audioDevices.length) {
    console.error('No audio devices found.');
  }

  return { videoDevices, audioDevices };
}

async function getCamera(deviceId) {
  // Use Max Width and Height
  return navigator.mediaDevices.getUserMedia({
    video: {
      deviceId: deviceId ? { exact: deviceId } : null,
    },
    audio: false,
  });
}

async function getMic(deviceId) {
  return navigator.mediaDevices.getUserMedia({
    video: false,
    audio: {
      deviceId: deviceId ? { exact: deviceId } : null,
    },
  });
}
```

### Camera Kit 세션 생성
<a name="integrating-snap-web-camera-kit-session"></a>

카메라 피드에 Lens를 적용하고 스테이지에 피드를 게시하기 위한 로직이 포함된 `stages.js`를 생성합니다. 다음 코드 블록을 복사하여 `stages.js`에 붙여넣는 것이 좋습니다. 그런 다음 코드 조각을 하나씩 검토하여 다음 섹션에서 어떤 일이 일어나고 있는지 이해할 수 있습니다.

#### JavaScript
<a name="integrating-snap-web-camera-kit-session-code"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

const {
  Stage,
  LocalStageStream,
  SubscribeType,
  StageEvents,
  ConnectionState,
  StreamType,
} = IVSBroadcastClient;

import {
  bootstrapCameraKit,
  createMediaStreamSource,
  Transform2D,
} from '@snap/camera-kit';

let cameraButton = document.getElementById('camera-control');
let micButton = document.getElementById('mic-control');
let joinButton = document.getElementById('join-button');
let leaveButton = document.getElementById('leave-button');

let controls = document.getElementById('local-controls');
let videoDevicesList = document.getElementById('video-devices');
let audioDevicesList = document.getElementById('audio-devices');

let lensSelector = document.getElementById('lens-selector');
let session;
let availableLenses = [];

// Stage management
let stage;
let joining = false;
let connected = false;
let localCamera;
let localMic;
let cameraStageStream;
let micStageStream;

const liveRenderTarget = document.getElementById('canvas');

const init = async () => {
  await initializeDeviceSelect();

  const cameraKit = await bootstrapCameraKit({
    apiToken: 'INSERT_YOUR_API_TOKEN_HERE',
  });

  session = await cameraKit.createSession({ liveRenderTarget });
  const { lenses } = await cameraKit.lensRepository.loadLensGroups([
    'INSERT_YOUR_LENS_GROUP_ID_HERE',
  ]);

  availableLenses = lenses;
  populateLensSelector(lenses);

  const snapStream = liveRenderTarget.captureStream();

  lensSelector.addEventListener('change', handleLensChange);
  lensSelector.disabled = true;
  cameraButton.addEventListener('click', () => {
    const isMuted = !cameraStageStream.isMuted;
    cameraStageStream.setMuted(isMuted);
    cameraButton.innerText = isMuted ? 'Show Camera' : 'Hide Camera';
  });

  micButton.addEventListener('click', () => {
    const isMuted = !micStageStream.isMuted;
    micStageStream.setMuted(isMuted);
    micButton.innerText = isMuted ? 'Unmute Mic' : 'Mute Mic';
  });

  joinButton.addEventListener('click', () => {
    joinStage(session, snapStream);
  });

  leaveButton.addEventListener('click', () => {
    leaveStage();
  });
};

async function setCameraKitSource(session, mediaStream) {
  const source = createMediaStreamSource(mediaStream);
  await session.setSource(source);
  source.setTransform(Transform2D.MirrorX);
  session.play();
}

const populateLensSelector = (lenses) => {
  lensSelector.innerHTML = '<option selected disabled>Choose Lens</option>';

  lenses.forEach((lens, index) => {
    const option = document.createElement('option');
    option.value = index;
    option.text = lens.name || `Lens ${index + 1}`;
    lensSelector.appendChild(option);
  });
};

const handleLensChange = (event) => {
  const selectedIndex = parseInt(event.target.value);
  if (session && availableLenses[selectedIndex]) {
    session.applyLens(availableLenses[selectedIndex]);
  }
};

const joinStage = async (session, snapStream) => {
  if (connected || joining) {
    return;
  }
  joining = true;

  const token = document.getElementById('token').value;

  if (!token) {
    window.alert('Please enter a participant token');
    joining = false;
    return;
  }

  // Retrieve the User Media currently set on the page
  localCamera = await getCamera(videoDevicesList.value);
  localMic = await getMic(audioDevicesList.value);
  await setCameraKitSource(session, localCamera);

  // Create StageStreams for Audio and Video
  cameraStageStream = new LocalStageStream(snapStream.getVideoTracks()[0]);
  micStageStream = new LocalStageStream(localMic.getAudioTracks()[0]);

  const strategy = {
    stageStreamsToPublish() {
      return [cameraStageStream, micStageStream];
    },
    shouldPublishParticipant() {
      return true;
    },
    shouldSubscribeToParticipant() {
      return SubscribeType.AUDIO_VIDEO;
    },
  };

  stage = new Stage(token, strategy);

  // Other available events:
  // https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#events
  stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {
    connected = state === ConnectionState.CONNECTED;

    if (connected) {
      joining = false;
      controls.classList.remove('hidden');
      lensSelector.disabled = false;
    } else {
      controls.classList.add('hidden');
      lensSelector.disabled = true;
    }
  });

  stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {
    console.log('Participant Joined:', participant);
  });

  stage.on(
    StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED,
    (participant, streams) => {
      console.log('Participant Media Added: ', participant, streams);

      let streamsToDisplay = streams;

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

      const videoEl = setupParticipant(participant);
      streamsToDisplay.forEach((stream) =>
        videoEl.srcObject.addTrack(stream.mediaStreamTrack)
      );
    }
  );

  stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant) => {
    console.log('Participant Left: ', participant);
    teardownParticipant(participant);
  });

  try {
    await stage.join();
  } catch (err) {
    joining = false;
    connected = false;
    console.error(err.message);
  }
};

const leaveStage = async () => {
  stage.leave();

  joining = false;
  connected = false;

  cameraButton.innerText = 'Hide Camera';
  micButton.innerText = 'Mute Mic';
  controls.classList.add('hidden');
};

init();
```

이 파일의 첫 번째 부분에서는 브로드캐스트 SDK와 Camera Kit 웹 SDK를 가져와서 각 SDK에 사용할 변수를 초기화합니다. [Camera Kit 웹 SDK를 부트스트래핑](https://kit.snapchat.com/reference/CameraKit/web/0.7.0/index.html#bootstrapping-the-sdk)한 후 `createSession`을 호출하여 Camera Kit 세션을 생성합니다. 캔버스 요소 객체는 세션으로 전달됩니다. 그러면 Camera Kit가 해당 캔버스로 렌더링하도록 지시합니다.

#### JavaScript
<a name="integrating-snap-web-camera-kit-session-code-2"></a>

```
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */

const {
  Stage,
  LocalStageStream,
  SubscribeType,
  StageEvents,
  ConnectionState,
  StreamType,
} = IVSBroadcastClient;

import {
  bootstrapCameraKit,
  createMediaStreamSource,
  Transform2D,
} from '@snap/camera-kit';

let cameraButton = document.getElementById('camera-control');
let micButton = document.getElementById('mic-control');
let joinButton = document.getElementById('join-button');
let leaveButton = document.getElementById('leave-button');

let controls = document.getElementById('local-controls');
let videoDevicesList = document.getElementById('video-devices');
let audioDevicesList = document.getElementById('audio-devices');

let lensSelector = document.getElementById('lens-selector');
let session;
let availableLenses = [];

// Stage management
let stage;
let joining = false;
let connected = false;
let localCamera;
let localMic;
let cameraStageStream;
let micStageStream;

const liveRenderTarget = document.getElementById('canvas');

const init = async () => {
  await initializeDeviceSelect();

  const cameraKit = await bootstrapCameraKit({
    apiToken: 'INSERT_YOUR_API_TOKEN_HERE',
  });

  session = await cameraKit.createSession({ liveRenderTarget });
```

### 렌즈 가져오기 및 렌즈 선택기 채우기
<a name="integrating-snap-web-fetch-apply-lens"></a>

렌즈를 가져오려면 렌즈 그룹 ID의 자리 표시자를 [Camera Kit 개발자 포털](https://camera-kit.snapchat.com/)에서 찾을 수 있는 자신의 ID로 교체합니다. 나중에 생성할 `populateLensSelector()` 함수를 사용하여 렌즈 선택 드롭다운을 채웁니다.

#### JavaScript
<a name="integrating-snap-web-fetch-apply-lens-code"></a>

```
session = await cameraKit.createSession({ liveRenderTarget });
  const { lenses } = await cameraKit.lensRepository.loadLensGroups([
    'INSERT_YOUR_LENS_GROUP_ID_HERE',
  ]);

  availableLenses = lenses;
  populateLensSelector(lenses);
```

### Camera Kit 세션에서 캔버스로 출력 렌더링
<a name="integrating-snap-web-render-output-to-canvas"></a>

[captureStream](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/captureStream) 메서드를 사용하여 캔버스의 콘텐츠 `MediaStream`을 반환합니다. 캔버스는 Lens가 적용된 카메라 피드의 비디오 스트림을 포함합니다. 카메라와 마이크를 음소거하는 버튼의 이벤트 리스너와 스테이지 참여 및 퇴장을 위한 이벤트 리스너도 추가할 수 있습니다. 스테이지 참여를 위한 이벤트 리스너에서는 Camera Kit 세션을 전달하고 캔버스에서 가져온 `MediaStream`을 스테이지에 게시할 수 있도록 전달합니다.

#### JavaScript
<a name="integrating-snap-web-render-output-to-canvas-code"></a>

```
const snapStream = liveRenderTarget.captureStream();

  lensSelector.addEventListener('change', handleLensChange);
  lensSelector.disabled = true;
  cameraButton.addEventListener('click', () => {
    const isMuted = !cameraStageStream.isMuted;
    cameraStageStream.setMuted(isMuted);
    cameraButton.innerText = isMuted ? 'Show Camera' : 'Hide Camera';
  });

  micButton.addEventListener('click', () => {
    const isMuted = !micStageStream.isMuted;
    micStageStream.setMuted(isMuted);
    micButton.innerText = isMuted ? 'Unmute Mic' : 'Mute Mic';
  });

  joinButton.addEventListener('click', () => {
    joinStage(session, snapStream);
  });

  leaveButton.addEventListener('click', () => {
    leaveStage();
  });
};
```

### 렌드 드롭다운을 채우는 함수 생성
<a name="integrating-snap-web-populate-lens-dropdown"></a>

다음 함수를 생성하여 이전에 가져온 렌즈로 **렌즈** 선택기를 채웁니다. **렌즈** 선택기는 페이지의 UI 요소로, 카메라 피드에 적용할 렌즈 목록에서 선택할 수 있습니다. 또한 지정된 렌즈를 **렌즈** 드롭다운에서 선택한 경우 `handleLensChange` 콜백 함수를 생성하여 지정된 렌즈를적용합니다.

#### JavaScript
<a name="integrating-snap-web-populate-lens-dropdown-code"></a>

```
const populateLensSelector = (lenses) => {
  lensSelector.innerHTML = '<option selected disabled>Choose Lens</option>';

  lenses.forEach((lens, index) => {
    const option = document.createElement('option');
    option.value = index;
    option.text = lens.name || `Lens ${index + 1}`;
    lensSelector.appendChild(option);
  });
};

const handleLensChange = (event) => {
  const selectedIndex = parseInt(event.target.value);
  if (session && availableLenses[selectedIndex]) {
    session.applyLens(availableLenses[selectedIndex]);
  }
};
```

### Camera Kit에 렌더링용 미디어 소스 제공 및 LocalStageStream 게시
<a name="integrating-snap-web-publish-localstagestream"></a>

Lens가 적용된 비디오 스트림을 게시하려면 이전에 캔버스에서 캡처한 `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)됩니다. 그 다음 미디어 소스에 Lens 효과가 적용되고 `session.play()`를 호출하여 출력 캔버스에 렌더링됩니다.

이제 캔버스에서 캡처한 `MediaStream`에 Lens를 적용한 다음 스테이지에 게시할 수 있습니다. 이렇게 하려면 `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`을 생성하고 다음 코드를 추가합니다. 지금까지 생성한 코드를 번들링하여 가져오기 문으로 Camera Kit를 사용할 수 있습니다.

#### JavaScript
<a name="integrating-snap-web-webpack-config-code"></a>

```
const path = require('path');
module.exports = {
  entry: ['./stage.js'],
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};
```

마지막으로 Webpack 구성 파일에 정의된 대로 JavaScript를 번들링하여 `npm run build`을 실행합니다. 테스트 목적으로 로컬 컴퓨터에서 HTML 및 JavaScript를 제공할 수 있습니다. 이 예제에서는 Python의 `http.server` 모듈을 사용합니다.

### HTTPS 서버 설정 및 테스트
<a name="integrating-snap-web-https-server-test"></a>

코드를 테스트하려면 HTTPS 서버를 설정해야 합니다. HTTPS 서버를 사용하여 웹 앱과 Snap Camera Kit SDK의 통합을 로컬 개발 및 테스트하면 CORS(교차 오리진 리소스 공유) 문제를 방지하는 데 도움이 됩니다.

터미널을 열고 이 시점까지 모든 코드를 생성한 디렉터리로 이동합니다. 다음 명령을 실행하여 자체 서명된 SSL/TLS 인증서와 프라이빗 키를 생성합니다.

```
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
```

이렇게 하면 `key.pem`(프라이빗 키)와 `cert.pem`(자체 서명된 인증서)라는 2개의 파일이 생성됩니다. 이름이 `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 인증서이기 때문에 웹 브라우저에서 신뢰할 수 없으므로 경고가 표시됩니다. 테스트 목적으로만 사용되므로 경고를 무시할 수 있습니다. 그러면 이전에 지정한 스냅 렌즈의 AR 효과가 카메라 피드에 적용된 것이 화면에 표시됩니다.

Python의 내장 `http.server` 및 `ssl` 모듈을 사용한 이 설정은 로컬 개발 및 테스트 목적에 적합하지만 프로덕션 환경에는 권장되지 않습니다. 이 설정에 사용되는 자체 서명된 SSL/TLS 인증서는 웹 브라우저 및 기타 클라이언트에서 신뢰할 수 없으므로 사용자가 서버에 액세스할 때 보안 경고가 발생합니다. 또한 이 예제에서는 Python의 내장 http.server 및 ssl 모듈을 사용하지만 다른 HTTPS 서버 솔루션을 사용하도록 선택할 수 있습니다.

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

Snap의 Camera Kit SDK를 IVS Android 브로드캐스트 SDK와 통합하려면 Camera Kit SDK를 설치하고, Camera Kit 세션을 초기화하고, Lens를 적용하고, Camera Kit 세션의 출력을 사용자 지정 이미지 입력 소스에 공급해야 합니다.

Camera Kit SDK를 설치하려면 모듈의 `build.gradle` 파일에 다음을 추가하세요. `$cameraKitVersion`을 [최신 Camera Kit SDK 버전](https://docs.snap.com/camera-kit/integrate-sdk/mobile/changelog-mobile)으로 교체하세요.

### Java
<a name="integrating-snap-android-install-camerakit-sdk-code"></a>

```
implementation "com.snap.camerakit:camerakit:$cameraKitVersion"
```

초기화하고 `cameraKitSession`을 가져오세요. 또한 Camera Kit는 Android의 [CameraX](https://developer.android.com/media/camera/camerax) API를 위한 편리한 래퍼를 제공하므로 Camera Kit로 CameraX를 사용하기 위해 복잡한 로직을 작성할 필요가 없습니다. `CameraXImageProcessorSource` 객체를 [ImageProcessor](https://snapchat.github.io/camera-kit-reference/api/android/latest/-camera-kit/com.snap.camerakit/-image-processor/index.html)의 [소스](https://snapchat.github.io/camera-kit-reference/api/android/latest/-camera-kit/com.snap.camerakit/-source/index.html)로 사용할 수 있으며, 이를 통해 카메라 미리보기 스트리밍 프레임을 시작할 수 있습니다.

### Java
<a name="integrating-snap-android-initialize-camerakitsession-code"></a>

```
 protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        // Camera Kit support implementation of ImageProcessor that is backed by CameraX library:
        // https://developer.android.com/training/camerax
        CameraXImageProcessorSource imageProcessorSource = new CameraXImageProcessorSource( 
            this /*context*/, this /*lifecycleOwner*/
        );
        imageProcessorSource.startPreview(true /*cameraFacingFront*/);

        cameraKitSession = Sessions.newBuilder(this)
                .imageProcessorSource(imageProcessorSource)
                .attachTo(findViewById(R.id.camerakit_stub))
                .build();
    }
```

### Lens 가져오기 및 적용
<a name="integrating-snap-android-fetch-apply-lenses"></a>

[Camera Kit 개발자 포털](https://camera-kit.snapchat.com/)의 Carousel에서 Lens를 구성하고 순서를 지정할 수 있습니다.

#### 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 Broadcast SDK와 함께 배경 교체 사용
<a name="broadcast-3p-camera-filters-background-replacement"></a>

배경 교체는 실시간 스트리밍 제작자가 배경을 변경할 수 있도록 하는 카메라 필터의 일종입니다. 다음 다이어그램에서 볼 수 있듯이 배경 교체 작업은 다음과 같습니다.

1. 라이브 카메라 피드에서 카메라 이미지를 가져옵니다.

1. Google ML Kit를 사용하여 전경 구성 요소와 배경 구성 요소로 구분합니다.

1. 생성된 분할 마스크를 사용자 지정 배경 이미지와 결합합니다.

1. 브로드캐스트용 사용자 지정 이미지 소스에 전달합니다.

![\[배경 교체 구현을 위한 워크플로입니다.\]](http://docs.aws.amazon.com/ko_kr/ivs/latest/RealTimeUserGuide/images/3P_Camera_Filters_Background_Replacement.png)


## 웹
<a name="background-replacement-web"></a>

이 섹션에서는 [웹 브로드캐스트 SDK를 사용하여 비디오를 게시 및 구독](https://docs.aws.amazon.com//ivs/latest/RealTimeUserGuide/getting-started-pub-sub-web.html)하는 방법을 이미 잘 알고 있다고 가정합니다.

라이브 스트림의 배경을 사용자 지정 이미지로 바꾸려면 [MediaPipe 이미지 Segmenter](https://developers.google.com/mediapipe/solutions/vision/image_segmenter)와 [셀카 분할 모델](https://developers.google.com/mediapipe/solutions/vision/image_segmenter#selfie-model)을 사용하세요. 이 기계 학습 모델은 비디오 프레임에서 전경 또는 배경에 있는 픽셀을 식별합니다. 그런 다음 비디오 피드의 전경 픽셀을 새 배경을 나타내는 사용자 지정 이미지에 복사하여 모델의 결과로 라이브 스트림의 배경을 바꿀 수 있습니다.

배경 교체를 IVS 실시간 스트리밍 웹 브로드캐스트 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
```

또한 `webpack`을 빌드 스크립트로 지정하도록 `package.json`을 업데이트해야 합니다.

#### 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 표준 문안을 생성하고 웹 브로드캐스트 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>

다음으로 본문 태그에 비디오 요소 하나와 캔버스 요소 두 개를 추가합니다. 비디오 요소는 라이브 카메라 피드를 포함하며 MediaPipe 이미지 Segmenter에 대한 입력으로 사용합니다. 첫 번째 캔버스 요소는 브로드캐스트될 피드의 미리보기를 렌더링하는 데 사용됩니다. 두 번째 캔버스 요소는 배경으로 사용할 사용자 지정 이미지를 렌더링하는 데 사용됩니다. 사용자 지정 이미지가 있는 두 번째 캔버스는 최종 캔버스에 프로그래밍 방식으로 픽셀을 복사하기 위한 소스로만 사용되므로 보기에서 숨겨집니다.

#### JavaScript
<a name="background-replacement-web-add-media-elements-code"></a>

```
<div class="row local-container">
      <video id="webcam" autoplay style="display: none"></video>
    </div>
    <div class="row local-container">
      <canvas id="canvas" width="640px" height="480px"></canvas>

      <div class="column" id="local-media"></div>
      <div class="static-controls hidden" id="local-controls">
        <button class="button" id="mic-control">Mute Mic</button>
        <button class="button" id="camera-control">Mute Camera</button>
      </div>
    </div>
    <div class="row local-container">
      <canvas id="background" width="640px" height="480px" style="display: none"></canvas>
    </div>
```

### 스크립트 태그 추가
<a name="background-replacement-web-add-script-tag"></a>

스크립트 태그를 추가하여 배경 교체 코드를 포함하는 번들 JavaScript 파일을 로드하고 스테이지에 게시합니다.

```
<script src="./dist/bundle.js"></script>
```

### app.js 생성
<a name="background-replacement-web-create-appjs"></a>

다음으로 JavaScript 파일을 생성하여 HTML 페이지에서 만든 캔버스 및 비디오 요소의 요소 객체를 가져옵니다. `ImageSegmenter` 및 `FilesetResolver` 모듈을 가져옵니다. `ImageSegmenter` 모듈은 분할 작업을 수행하는 데 사용됩니다.

#### JavaScript
<a name="create-appjs-import-imagesegmenter-fileresolver-code"></a>

```
const canvasElement = document.getElementById("canvas");
const background = document.getElementById("background");
const canvasCtx = canvasElement.getContext("2d");
const backgroundCtx = background.getContext("2d");
const video = document.getElementById("webcam");

import { ImageSegmenter, FilesetResolver } from "@mediapipe/tasks-vision";
```

다음으로, 사용자 카메라에서 MediaStream을 검색하는 `init()` 함수를 생성하고, 카메라 프레임 로드가 완료될 때마다 콜백 함수를 호출합니다. 스테이지에 참여 및 퇴장할 수 있는 버튼에 이벤트 리스너를 추가합니다.

스테이지에 참가할 때는 `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`을 비디오 객체에 할당합니다. 나중에 이 비디오 객체는 이미지 Segmenter로 전달됩니다. 또한 비디오 프레임 로드가 완료될 때마다 호출되도록 콜백 함수로서 이름이 `renderVideoToCanvas`인 함수를 설정하세요. 이후 단계에서 이 함수를 정의할 것입니다.

#### JavaScript
<a name="background-replacement-web-load-background-image-code"></a>

```
initBackgroundCanvas();

  video.srcObject = localCamera;
  video.addEventListener("loadeddata", renderVideoToCanvas);
```

로컬 파일에서 이미지를 로드하는 `initBackgroundCanvas` 함수를 구현해 보겠습니다. 이 예에서는 해변 이미지를 사용자 지정 배경으로 사용합니다. 사용자 지정 이미지가 있는 캔버스는 카메라 피드를 포함한 캔버스 요소의 전경 픽셀과 병합되므로 디스플레이에서 숨겨집니다.

#### JavaScript
<a name="background-replacement-web-implement-initBackgroundCanvas-code"></a>

```
const initBackgroundCanvas = () => {
  let img = new Image();
  img.src = "beach.jpg";

  img.onload = () => {
    backgroundCtx.clearRect(0, 0, canvas.width, canvas.height);
    backgroundCtx.drawImage(img, 0, 0);
  };
};
```

### ImageSegmenter 인스턴스 생성
<a name="background-replacement-web-imagesegmenter"></a>

다음으로 이미지를 분할하고 결과를 마스크로 반환하는 `ImageSegmenter` 인스턴스를 생성합니다. `ImageSegmenter`의 인스턴스를 생성할 때는 [셀카 분할 모델](https://developers.google.com/mediapipe/solutions/vision/image_segmenter#selfie-model)을 사용하게 됩니다.

#### JavaScript
<a name="background-replacement-web-imagesegmenter-code"></a>

```
const createImageSegmenter = async () => {
  const audio = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.2/wasm");

  imageSegmenter = await ImageSegmenter.createFromOptions(audio, {
    baseOptions: {
      modelAssetPath: "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite",
      delegate: "GPU",
    },
    runningMode: "VIDEO",
    outputCategoryMask: true,
  });
};
```

### 캔버스로 비디오 피드 렌더링
<a name="background-replacement-web-render-video-to-canvas"></a>

다음으로, 비디오 피드를 다른 캔버스 요소에 렌더링하는 함수를 생성합니다. Canvas 2D API를 사용하여 전경 픽셀을 추출하려면 비디오 피드를 캔버스로 렌더링해야 합니다. 이 작업을 수행하는 동안 [segmentforVideo](https://developers.google.com/mediapipe/api/solutions/js/tasks-vision.imagesegmenter#imagesegmentersegmentforvideo) 메서드를 사용하여 비디오 프레임의 배경에서 전경을 구분하여 비디오 프레임을 `ImageSegmenter` 인스턴스로 전달합니다. [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_version}`과 `${google_ml_kit_version}`을 각각 최신 버전의 [CameraX](https://developer.android.com/jetpack/androidx/releases/camera) 및 [Google ML Kit](https://developers.google.com/ml-kit/vision/selfie-segmentation/android) 라이브러리로 교체하세요.

#### Java
<a name="background-replacement-android-install-camerax-googleml-code"></a>

```
implementation "com.google.mlkit:segmentation-selfie:${google_ml_kit_version}"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
```

다음 라이브러리를 가져옵니다.

#### Java
<a name="background-replacement-android-import-libraries-code"></a>

```
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider
import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions
```

### 표준 문안 변수 초기화
<a name="background-replacement-android-initialize-variables"></a>

`ImageAnalysis`의 인스턴스와 다음 `ExecutorService`의 인스턴스를 초기화합니다.

#### Java
<a name="background-replacement-android-initialize-imageanalysis-executorservice-code"></a>

```
private lateinit var binding: ActivityMainBinding
private lateinit var cameraExecutor: ExecutorService
private var analysisUseCase: ImageAnalysis? = null
```

[STREAM\$1MODE](https://developers.google.com/ml-kit/vision/selfie-segmentation/android#detector_mode)에서 Segmenter 인스턴스를 초기화하세요.

#### Java
<a name="background-replacement-android-initialize-segmenter-code"></a>

```
private val options =
        SelfieSegmenterOptions.Builder()
            .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
            .build()

private val segmenter = Segmentation.getClient(options)
```

### 사용자 지정 이미지 소스 생성
<a name="background-replacement-android-create-image-source"></a>

활동의 `onCreate` 메서드에서 `DeviceDiscovery` 개체의 인스턴스를 생성하고 사용자 지정 이미지 소스를 생성하세요. 사용자 지정 이미지 소스에서 제공한 `Surface`로 사용자 지정 배경 이미지 위에 전경이 오버레이된 최종 이미지를 받게 됩니다. 그런 다음 사용자 지정 이미지 소스를 사용하여 `ImageLocalStageStream`의 인스턴스를 생성합니다. 그러면 `ImageLocalStageStream`의 인스턴스(이 예제에서는 `filterStream`으로 이름이 지정됨)를 스테이지에 게시할 수 있습니다. 스테이지 설정에 대한 지침은 [IVS Android 브로드캐스트 SDK 가이드](broadcast-android.md)를 참조하세요. 마지막으로 카메라를 관리하는 데 사용할 스레드도 생성하세요.

#### Java
<a name="background-replacement-android-create-image-source-code"></a>

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

cameraExecutor = Executors.newSingleThreadExecutor()
```

### 카메라 프레임 관리
<a name="background-replacement-android-camera-frames"></a>

다음으로 카메라를 초기화하는 함수를 생성합니다. 이 함수는 CameraX 라이브러리를 사용하여 라이브 카메라 피드에서 이미지를 추출합니다. 먼저 `cameraProviderFuture`라는 `ProcessCameraProvider` 인스턴스를 생성합니다. 이 객체는 카메라 공급자를 확보한 미래의 결과를 나타냅니다. 그런 다음 프로젝트에서 이미지를 비트맵으로 로드합니다. 이 예제에서는 해변 이미지를 배경으로 사용하지만 원하는 어떤 이미지라도 사용할 수 있습니다.

그런 다음 `cameraProviderFuture`에 리스너를 추가합니다. 카메라를 사용할 수 있게 되거나 카메라 공급자를 구하는 과정에서 오류가 발생하면 이 리스너에 알림이 전송됩니다.

#### Java
<a name="background-replacement-android-initialize-camera-code"></a>

```
private fun startCamera(surface: Surface) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        val imageResource = R.drawable.beach
        val bgBitmap: Bitmap = BitmapFactory.decodeResource(resources, imageResource)
        var resultBitmap: Bitmap;


        cameraProviderFuture.addListener({
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            
                if (mediaImage != null) {
                    val inputImage =
                        InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

                            resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
                            canvas = surface.lockCanvas(null);
                            canvas.drawBitmap(resultBitmap, 0f, 0f, null)

                            surface.unlockCanvasAndPost(canvas);

                        }
                        .addOnFailureListener { exception ->
                            Log.d("App", exception.message!!)
                        }
                        .addOnCompleteListener {
                            imageProxy.close()
                        }

                }
            };

            val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase)

            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }
```

리스너 내에서 라이브 카메라 피드의 각 개별 프레임에 액세스할 수 있도록 `ImageAnalysis.Builder`를 생성하세요. 백 프레셔 전략을 `STRATEGY_KEEP_ONLY_LATEST`로 설정합니다. 이렇게 하면 한 번에 하나의 카메라 프레임만 처리에 전송되도록 할 수 있습니다. 각 개별 카메라 프레임을 비트맵으로 변환하면 픽셀을 추출하여 나중에 사용자 지정 배경 이미지와 결합할 수 있습니다.

#### Java
<a name="background-replacement-android-create-imageanalysisbuilder-code"></a>

```
val imageAnalyzer = ImageAnalysis.Builder()
analysisUseCase = imageAnalyzer
    .setTargetResolution(Size(360, 640))
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()

analysisUseCase?.setAnalyzer(cameraExecutor) { imageProxy: ImageProxy ->
    val mediaImage = imageProxy.image
    val tempBitmap = imageProxy.toBitmap();
    val inputBitmap = tempBitmap.rotate(imageProxy.imageInfo.rotationDegrees.toFloat())
```

### 카메라 프레임을 Google ML Kit로 전달
<a name="background-replacement-android-frames-to-mlkit"></a>

다음으로, `InputImage`를 생성하여 처리를 위한 Segmenter 인스턴스에 전달합니다. `ImageAnalysis`의 인스턴스에서 제공하는 `ImageProxy`로 `InputImage`를 생성할 수 있습니다. Segmenter에 `InputImage`가 제공되면 픽셀이 전경이나 배경에 있을 가능성을 나타내는 신뢰도 점수가 포함된 마스크를 반환합니다. 이 마스크는 너비 및 높이 속성도 제공하며, 이 속성을 사용하여 이전에 로드한 사용자 지정 배경 이미지의 배경 픽셀을 포함하는 새 배열을 생성할 수 있습니다.

#### Java
<a name="background-replacement-android-frames-to-mlkit-code"></a>

```
if (mediaImage != null) {
        val inputImage =
            InputImage.fromMediaImag


segmenter.process(inputImage)
    .addOnSuccessListener { segmentationMask ->
        val mask = segmentationMask.buffer
        val maskWidth = segmentationMask.width
        val maskHeight = segmentationMask.height
        val backgroundPixels = IntArray(maskWidth * maskHeight)
        bgBitmap.getPixels(backgroundPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)
```

### 카메라 프레임 전경을 사용자 지정 배경에 오버레이
<a name="background-replacement-android-overlay-frame-foreground"></a>

신뢰도 점수가 포함된 마스크, 비트맵의 카메라 프레임, 사용자 지정 배경 이미지의 색상 픽셀을 사용하면 전경을 사용자 지정 배경에 오버레이하는 데 필요한 모든 것을 얻을 수 있습니다. 그 다음 파라미터로 `overlayForeground` 함수를 호출합니다.

#### Java
<a name="background-replacement-android-call-overlayforeground-code"></a>

```
resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
```

이 함수는 마스크를 반복하고 신뢰도 값을 확인하여 배경 이미지에서 해당 픽셀 색상을 가져올지 아니면 카메라 프레임에서 가져올지 결정합니다. 신뢰도 값이 마스크의 픽셀이 배경에 있을 가능성이 높다는 것을 나타내면 배경 이미지에서 해당 픽셀 색상을 가져오고, 그렇지 않으면 카메라 프레임에서 해당 픽셀 색상을 가져와 전경을 만듭니다. 함수가 마스크 반복을 마치면 새 색상 픽셀 배열을 사용하여 새 비트맵을 생성하고 반환합니다. 이 새 비트맵은 사용자 지정 배경에 오버레이된 전경을 포함하고 있습니다.

#### Java
<a name="background-replacement-android-run-overlayforeground-code"></a>

```
private fun overlayForeground(
        byteBuffer: ByteBuffer,
        maskWidth: Int,
        maskHeight: Int,
        cameraBitmap: Bitmap,
        backgroundPixels: IntArray
    ): Bitmap {
        @ColorInt val colors = IntArray(maskWidth * maskHeight)
        val cameraPixels = IntArray(maskWidth * maskHeight)

        cameraBitmap.getPixels(cameraPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)

        for (i in 0 until maskWidth * maskHeight) {
            val backgroundLikelihood: Float = 1 - byteBuffer.getFloat()

            // Apply the virtual background to the color if it's not part of the foreground
            if (backgroundLikelihood > 0.9) {
                // Get the corresponding pixel color from the background image
                // Set the color in the mask based on the background image pixel color
                colors[i] = backgroundPixels.get(i)
            } else {
                // Get the corresponding pixel color from the camera frame
                // Set the color in the mask based on the camera image pixel color
                colors[i] = cameraPixels.get(i)
            }
        }

        return Bitmap.createBitmap(
            colors, maskWidth, maskHeight, Bitmap.Config.ARGB_8888
        )
    }
```

### 새 이미지를 사용자 지정 이미지 소스에 제공
<a name="background-replacement-android-custom-image-source"></a>

그런 다음 사용자 지정 이미지 소스에서 제공한 `Surface`에 새 비트맵을 쓸 수 있습니다. 그러면 스테이지로 브로드캐스트합니다.

#### Java
<a name="background-replacement-android-custom-image-source-code"></a>

```
resultBitmap = overlayForeground(mask, inputBitmap, mutableBitmap, bgBitmap)
canvas = surface.lockCanvas(null);
canvas.drawBitmap(resultBitmap, 0f, 0f, null)
```

카메라 프레임을 가져와서 Segmenter로 전달하고 백그라운드에 오버레이하는 전체 기능은 다음과 같습니다.

#### Java
<a name="background-replacement-android-custom-image-source-startcamera-code"></a>

```
@androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class)
    private fun startCamera(surface: Surface) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        val imageResource = R.drawable.clouds
        val bgBitmap: Bitmap = BitmapFactory.decodeResource(resources, imageResource)
        var resultBitmap: Bitmap;

        cameraProviderFuture.addListener({
            // Used to bind the lifecycle of cameras to the lifecycle owner
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            val imageAnalyzer = ImageAnalysis.Builder()
            analysisUseCase = imageAnalyzer
                .setTargetResolution(Size(720, 1280))
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()

            analysisUseCase!!.setAnalyzer(cameraExecutor) { imageProxy: ImageProxy ->
                val mediaImage = imageProxy.image
                val tempBitmap = imageProxy.toBitmap();
                val inputBitmap = tempBitmap.rotate(imageProxy.imageInfo.rotationDegrees.toFloat())

                if (mediaImage != null) {
                    val inputImage =
                        InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

                    segmenter.process(inputImage)
                        .addOnSuccessListener { segmentationMask ->
                            val mask = segmentationMask.buffer
                            val maskWidth = segmentationMask.width
                            val maskHeight = segmentationMask.height
                            val backgroundPixels = IntArray(maskWidth * maskHeight)
                            bgBitmap.getPixels(backgroundPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)

                            resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
                            canvas = surface.lockCanvas(null);
                            canvas.drawBitmap(resultBitmap, 0f, 0f, null)

                            surface.unlockCanvasAndPost(canvas);

                        }
                        .addOnFailureListener { exception ->
                            Log.d("App", exception.message!!)
                        }
                        .addOnCompleteListener {
                            imageProxy.close()
                        }

                }
            };

            val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase)

            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }
```

# IVS 브로드캐스트 SDK: 모바일 오디오 모드 \$1 실시간 스트리밍
<a name="broadcast-mobile-audio-modes"></a>

오디오 품질은 실제 팀 미디어 경험의 중요한 부분이며, 모든 사용 사례에 가장 적합한 단일 오디오 구성은 없습니다. 사용자가 IVS 실시간 스트림을 청취할 때 최상의 경험을 할 수 있도록 모바일 SDK는 몇 가지 사전 설정 오디오 구성과 필요에 따라 더욱 강력한 사용자 지정을 제공합니다.

## 소개
<a name="broadcast-mobile-audio-modes-introduction"></a>

IVS 모바일 브로드캐스트 SDK는 `StageAudioManager` 클래스를 제공합니다. 이 클래스는 두 플랫폼에서 기본 오디오 모드를 제어할 수 있는 단일 접점이 되도록 설계되었습니다. Android에서 이를 통해 오디오 모드, 오디오 소스, 콘텐츠 유형, 사용량, 통신 디바이스 등 [AudioManager](https://developer.android.com/reference/android/media/AudioManager)를 제어합니다. iOS에서 애플리케이션 [AVAudioSession](https://developer.apple.com/documentation/avfaudio/avaudiosession)을 제어하고 [voiceProcessing](https://developer.apple.com/documentation/avfaudio/avaudioionode/3152101-voiceprocessingenabled?language=objc)의 활성화 여부도 제어합니다.

**중요**: IVS 실시간 브로드캐스트 SDK가 활성화되어 있는 동안에는 `AudioManager` 또는 `AVAudioSession`와 직접 상호 작용하지 마세요. 이렇게 하면 오디오가 손실되거나 오디오가 잘못된 디바이스에서 녹음되거나 재생될 수 있습니다.

첫 번째 `DeviceDiscovery` 또는 `Stage` 객체를 생성하기 전에 `StageAudioManager` 클래스를 구성해야 합니다.

------
#### [ Android (Kotlin) ]

```
StageAudioManager.getInstance(context).setPreset(StageAudioManager.UseCasePreset.VIDEO_CHAT) // The default value

val deviceDiscovery = DeviceDiscovery(context)
val stage = Stage(context, token, this)

// Other Stage implementation code
```

------
#### [ iOS (Swift) ]

```
IVSStageAudioManager.sharedInstance().setPreset(.videoChat) // The default value

let deviceDiscovery = IVSDeviceDiscovery()
let stage = try? IVSStage(token: token, strategy: self)

// Other Stage implementation code
```

------

`DeviceDiscovery` 또는 `Stage` 인스턴스를 초기화하기 전에 `StageAudioManager`에 아무것도 설정하지 않은 경우 `VideoChat` 사전 설정이 자동으로 적용됩니다.

## 오디오 모드 사전 설정
<a name="broadcast-mobile-audio-modes-presets"></a>

실시간 브로드캐스트 SDK는 아래 설명과 같이 일반적인 사용 사례에 맞게 조정된 세 가지 사전 설정을 제공합니다. 각 사전 설정에 대해 사전 설정을 서로 차별화하는 다섯 가지 주요 카테고리를 다룹니다.

**볼륨 로커** 범주는 디바이스의 물리적 볼륨 로커를 통해 사용되거나 변경되는 볼륨 유형(미디어 볼륨 또는 직접 호출 볼륨)을 나타냅니다. 참고로, 오디오 모드 전환 시 볼륨이 이 영향을 받습니다. 예를 들어, 비디오 채팅 사전 설정을 사용하는 동안 디바이스 볼륨이 최대값으로 설정되어 있다고 가정해 보겠습니다. 구독 전용 사전 설정으로 전환하면 운영 체제와 볼륨 수준이 달라져서 디바이스의 볼륨이 상당히 변경될 수 있습니다.

### 비디오 채팅
<a name="audio-modes-presets-video-chat"></a>

로컬 디바이스가 다른 참가자와 실시간으로 대화할 때 사용하도록 설계된 기본 사전 설정입니다.

**iOS의 알려진 문제 **: 이 사전 설정을 사용하고 마이크를 연결하지 않으면 디바이스 스피커 대신에 이어폰을 통해 오디오가 재생됩니다. 이 사전 설정은 마이크와 결합한 경우에만 사용하세요.


| 카테고리 | Android | iOS | 
| --- | --- | --- | 
| 에코 소거 | 활성화됨 | 활성화됨 | 
| 볼륨 로커 | 호출 볼륨 | 호출 볼륨 | 
| 마이크 선택 | OS에 따라 제한됩니다. USB 마이크를 사용할 수 없을 수도 있습니다. | OS에 따라 제한됩니다. USB 및 Bluetooth 마이크를 사용할 수 없을 수도 있습니다. 입력과 출력을 함께 처리하는 Bluetooth 헤드셋이 작동해야 합니다(예: AirPods). | 
| 오디오 출력 | 모든 출력 디바이스가 작동해야 합니다. | OS에 따라 제한됩니다. 유선 헤드셋은 사용할 수 없을 수도 있습니다. | 
| 오디오 품질 | 중간/낮음 미디어 재생이 아니라 전화 통화처럼 들릴 것입니다. | 중간/낮음 미디어 재생이 아니라 전화 통화처럼 들릴 것입니다. | 

### 구독 전용
<a name="audio-modes-presets-subscribe-only"></a>

이 사전 설정은 다른 게시 참가자를 구독하고 직접 게시하지는 않으려는 경우에 적합합니다. 오디오 품질에 초점을 맞추고 사용 가능한 모든 출력 디바이스를 지원합니다.


| 카테고리 | Android | iOS | 
| --- | --- | --- | 
| 에코 소거 | 비활성화됨 | 비활성화됨 | 
| 볼륨 로커 | 미디어 볼륨 | 미디어 볼륨 | 
| 마이크 선택 | 해당 없음, 이 사전 설정은 게시용으로 설계되지 않았습니다. | 해당 없음, 이 사전 설정은 게시용으로 설계되지 않았습니다. | 
| 오디오 출력 | 모든 출력 디바이스가 작동해야 합니다. | 모든 출력 디바이스가 작동해야 합니다. | 
| 오디오 품질 | 높음 음악을 포함한 모든 미디어 유형은 선명하게 전달되어야 합니다. | 높음 음악을 포함한 모든 미디어 유형은 선명하게 전달되어야 합니다. | 

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

이 사전 설정은 게시 기능은 그대로 유지하면서 고품질 구독을 할 수 있도록 설계되었습니다. 에코 소거 기능을 제공하려면 레코딩 및 재생 하드웨어가 필요합니다. 여기 사용 사례에서는 USB 마이크와 유선 헤드셋을 사용합니다. SDK는 이러한 디바이스가 에코를 발생시키지 않도록 물리적으로 분리함으로써 최고 품질의 오디오를 유지합니다.


| 카테고리 | Android | iOS | 
| --- | --- | --- | 
| 에코 소거 | 플랫폼 에코 취소는 비활성화되어 있지만 `StageAudioConfiguration.enableEchoCancellation`이 true이면 소프트웨어 에코 취소가 계속 발생할 수 있습니다. | 비활성화됨 | 
| 볼륨 로커 | 대부분의 경우에는 미디어 볼륨입니다. Bluetooth 마이크 연결 시 통화 볼륨입니다. | 미디어 볼륨 | 
| 마이크 선택 | 모든 마이크가 작동해야 합니다. | 모든 마이크가 작동해야 합니다. | 
| 오디오 출력 | 모든 출력 디바이스가 작동해야 합니다. | 모든 출력 디바이스가 작동해야 합니다. | 
| 오디오 품질 | 높음 양쪽 모두 음악을 전송하고 반대편에서도 선명하게 들을 수 있어야 합니다. Bluetooth 헤드셋을 연결하면 Bluetooth SCO 모드가 활성화되면 오디오 품질이 떨어집니다. | 높음 양쪽 모두 음악을 전송하고 반대편에서도 선명하게 들을 수 있어야 합니다. Bluetooth 헤드셋을 연결하면 헤드셋에 따라 Bluetooth SCO 모드가 활성화되어 오디오 음질이 떨어질 수 있습니다. | 

## 고급 사용 사례
<a name="broadcast-mobile-audio-modes-advanced-use-cases"></a>

사전 설정 외에도 iOS 및 Android 실시간 스트리밍 브로드캐스트 SDK를 통해 기본 플랫폼 오디오 모드를 구성할 수 있습니다.
+ Android에서 [AudioSource](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource), [Usage](https://developer.android.com/reference/android/media/AudioAttributes#USAGE_ALARM) 및 [ContentType](https://developer.android.com/reference/android/media/AudioAttributes#CONTENT_TYPE_MOVIE)을 설정합니다.
+ iOS에서 [AVAudioSession.Category](https://developer.apple.com/documentation/avfaudio/avaudiosession/category), [AVAudioSession.CategoryOptions](https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions), [AVAudioSession.Mode](https://developer.apple.com/documentation/avfaudio/avaudiosession/mode) 그리고 게시하는 동안 [음성 처리](https://developer.apple.com/documentation/avfaudio/avaudioionode/3152101-voiceprocessingenabled?language=objc)가 활성화되었는지 여부를 전환하는 기능을 사용할 수 있습니다.

참고: 이러한 오디오 SDK 메서드를 사용하는 경우 기본 오디오 세션을 잘못 구성할 수 있습니다. 예를 들어, iOS의 `.allowBluetooth` 옵션을 `.playback` 범주와 결합하여 사용하면 유효하지 않은 오디오 구성이 생성되고 SDK에서 오디오를 레코딩하거나 재생할 수 없습니다. 이러한 방법은 애플리케이션에 검증된 특정 오디오-세션 요구 사항이 있는 경우에만 사용하도록 설계되었습니다.

------
#### [ Android (Kotlin) ]

```
// This would act similar to the Subscribe Only preset, but it uses a different ContentType.
StageAudioManager.getInstance(context)
    .setConfiguration(StageAudioManager.Source.GENERIC,
                      StageAudioManager.ContentType.MOVIE,
                      StageAudioManager.Usage.MEDIA);

val stage = Stage(context, token, this)

// Other Stage implementation code
```

------
#### [ iOS (Swift) ]

```
// This would act similar to the Subscribe Only preset, but it uses a different mode and options.
IVSStageAudioManager.sharedInstance()
    .setCategory(.playback,
                 options: [.duckOthers, .mixWithOthers],
                 mode: .default)

let stage = try? IVSStage(token: token, strategy: self)

// Other Stage implementation code
```

------

### iOS 에코 취소
<a name="advanced-use-cases-ios_echo_cancellation"></a>

iOS의 에코 취소는 `echoCancellationEnabled` 메서드를 사용하여 `IVSStageAudioManager`를 통해 독립적으로 제어할 수도 있습니다. 이 메서드에서는 SDK에서 사용하는 기본 `AVAudioEngine`의 입력 및 출력 노드에서 [음성 처리](https://developer.apple.com/documentation/avfaudio/avaudioionode/3152101-voiceprocessingenabled?language=objc)가 활성화되는지 여부를 제어합니다. 수동으로 이 속성을 변경하는 경우의 영향을 이해하는 것이 중요합니다.
+ `AVAudioEngine` 속성은 SDK의 마이크가 활성 상태인 경우에만 적용됩니다. 이는 입력 노드와 출력 노드에서 모두 음성 처리가 동시에 활성화되어야 한다는 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에서는 SDK의 마이크가 사용되지 않더라도 오디오 재생을 위한 내부 `AVAudioEngine` 인스턴스를 계속 관리합니다. 따라서 `IVSStageAudioManager`에 제공된 값이 사용자 지정 오디오 소스에서 제공되는 오디오와 호환되어야 합니다.

게시에 사용되는 사용자 지정 오디오 소스가 마이크에서 레코딩되지만 호스트 애플리케이션에서 관리하는 경우 SDK 관리형 마이크가 활성화되지 않으면 위의 에코 취소 SDK가 작동하지 않습니다. 해당 요구 사항을 해결하려면 [iOS 에코 취소](#advanced-use-cases-ios_echo_cancellation)를 참조하세요.

### Android에서 Bluetooth로 게시
<a name="advanced-use-cases-bluetooth-android"></a>

다음 조건이 충족되면 SDK가 Android의 `VIDEO_CHAT` 사전 설정을 자동으로 되돌립니다.
+ 할당된 구성은 `VOICE_COMMUNICATION` 사용량 값을 사용하지 않습니다.
+ Bluetooth 마이크가 디바이스에 연결되어 있습니다.
+ 로컬 참가자가 스테이지에 게시하고 있습니다.

이는 오디오 레코딩에 Bluetooth 헤드셋을 사용하는 방식과 관련된 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가 사용하는 동안 Agora 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)`를 호출할 수 있습니다.