

# VS Broadcast SDK で Snap を使用する
<a name="broadcast-3p-camera-filters-integrating-snap"></a>

このドキュメントでは、IVS Broadcast SDK で Snap の Camera Kit SDK を使用する方法について説明します。

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

このセクションは、読者が既に [Web Broadcast SDK を使用した動画の公開とサブスクリプション](getting-started-pub-sub-web.md)に慣れていることを前提としています。

Snap の Camera Kit SDK を IVS Real-Time Streaming Web Broadcast 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 共通スクリプトを作成し、Web Broadcast SDK をスクリプトタグとしてインポートします。次のコードでは、`<SDK version>` を、使用している Broadcast SDK のバージョンに必ず置き換えてください。

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

```
<!--
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */
-->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <title>Amazon IVS Real-Time Streaming Web Sample (HTML and JavaScript)</title>

  <!-- Fonts and Styling -->
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css" />
  <link rel="stylesheet" href="./index.css" />

  <!-- Stages in Broadcast SDK -->
  <script src="https://web-broadcast.live-video.net/<SDK version>/amazon-ivs-web-broadcast.js"></script>
</head>

<body>
  <!-- Introduction -->
  <header>
    <h1>Amazon IVS Real-Time Streaming Web Sample (HTML and JavaScript)</h1>

    <p>This sample is used to demonstrate basic HTML / JS usage. <b><a href="https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/multiple-hosts.html">Use the AWS CLI</a></b> to create a <b>Stage</b> and a corresponding <b>ParticipantToken</b>. Multiple participants can load this page and put in their own tokens. You can <b><a href="https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#glossary" target="_blank">read more about stages in our public docs.</a></b></p>
  </header>
  <hr />
  
  <!-- Setup Controls -->
 
  <!-- Display Local Participants -->
  
  <!-- Lens Selector -->

  <!-- Display Remote Participants -->

  <!-- Load All Desired Scripts -->
```

### セットアップ要素を追加する
<a name="integrating-snap-web-add-setup-elements"></a>

カメラ、マイク、およびレンズを選択し、参加者トークンを指定するための HTML を次のように作成します。

#### HTML
<a name="integrating-snap-web-setup-controls-code"></a>

```
<!-- Setup Controls -->
  <div class="row">
    <div class="column">
      <label for="video-devices">Select Camera</label>
      <select disabled id="video-devices">
        <option selected disabled>Choose Option</option>
      </select>
    </div>
    <div class="column">
      <label for="audio-devices">Select Microphone</label>
      <select disabled id="audio-devices">
        <option selected disabled>Choose Option</option>
      </select>
    </div>
    <div class="column">
      <label for="token">Participant Token</label>
      <input type="text" id="token" name="token" />
    </div>
    <div class="column" style="display: flex; margin-top: 1.5rem">
      <button class="button" style="margin: auto; width: 100%" id="join-button">Join Stage</button>
    </div>
    <div class="column" style="display: flex; margin-top: 1.5rem">
      <button class="button" style="margin: auto; width: 100%" id="leave-button">Leave Stage</button>
    </div>
  </div>
```

その下に HTML を追加して、ローカルおよびリモートの参加者からのカメラフィードを表示します。

#### HTML
<a name="integrating-snap-web-local-remote-participants-code"></a>

```
 <!-- Local Participant -->
<div class="row local-container">
    <canvas id="canvas"></canvas>

    <div class="column" id="local-media"></div>
    <div class="static-controls hidden" id="local-controls">
      <button class="button" id="mic-control">Mute Mic</button>
      <button class="button" id="camera-control">Mute Camera</button>
    </div>
  </div>

  
  <hr style="margin-top: 5rem"/>
  
  <!-- Remote Participants -->
  <div class="row">
    <div id="remote-media"></div>
  </div>
```

カメラをセットアップするためのヘルパーメソッドなどの追加のロジックやバンドルされている JavaScript ファイルをロードします。(このセクションの後半では、これらの JavaScript ファイルを作成して 1 つのファイルにバンドルします。これにより、Camera Kit をモジュールとしてインポートできます。バンドルされている JavaScript ファイルには、Camera Kit の設定、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>

`stages.js` を作成します。これには、カメラフィードに Lens を適用し、そのフィードをステージに公開するためのロジックが含まれます。次のコードブロックをコピーして `stages.js` に貼り付けることをお勧めします。その後、コードの内容を 1 つずつ確認すると、後続のセクションで何が起こっているか把握できます。

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

このファイルの最初の部分では、Broadcast SDK と Camera Kit Web SDK をインポートし、各 SDK で使用する変数を初期化します。[Camera Kit Web 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>

レンズを取得するには、Lens グループ 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 を適用したビデオストリームを公開するには、以前にキャンバスからキャプチャした `MediaStream` を渡す `setCameraKitSource` という名前の関数を作成します。ローカルカメラフィードをまだ組み込んでいないので、キャンバスからの `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` を作成して次のコードを追加します。これにより、これまでに作成したコードがバンドルされ、import ステートメントを使用して 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'),
  },
};
```

最後に、`npm run build` を実行して、Webpack 設定ファイルで定義されている JavaScript をバンドルします。テスト目的であれば、ローカルコンピュータから HTML と JavaScript を提供することができます。この例では、Python の `http.server` モジュールを使用します。

### HTTPS サーバーのセットアップとテスト
<a name="integrating-snap-web-https-server-test"></a>

コードをテストするには、HTTPS サーバーをセットアップする必要があります。ウェブアプリケーションの Snap Camera Kit SDK との統合をローカルで開発およびテストするために HTTPS サーバーを使用すると、オリジン間リソース共有 (CORS、Cross-Origin Resource Sharing) の問題を回避できます。

ターミナルを開き、この時点までのすべてのコードを作成したディレクトリに移動します。次のコマンドを実行して、自己署名 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 Broadcast 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/)で、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
```