

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