# サンプル




## 概要

ここでは Sora JavaScript SDK を利用したサンプルを紹介します。

## Vite

このサンプルでは [Vite](https://vite.dev/) を使用して、環境変数から接続先の情報を取得するようにしています。

[Env Variables and Modes | Vite](https://vite.dev/guide/env-and-mode)

### .env または .env.local

```bash
# Sora シグナリング URL
VITE_SORA_SIGNALING_URL=
# Sora チャネル ID
VITE_SORA_CHANNEL_ID=
# Sora チャネル ID プレフィックス
VITE_SORA_CHANNEL_ID_PREFIX=
# Sora チャネル ID サフィックス
VITE_SORA_CHANNEL_ID_SUFFIX=
# JWT 生成用のシークレットキー
VITE_SECRET_KEY=
```

### VITE_SORA_SIGNALING_URL

Sora のシグナリング URL を指定してください。

- Sora 自前の場合は設定済のシグナリング URL を指定してください- `VITE_SORA_SIGNALING_URL=wss://sora.example.com/signaling`
- Sora Labo の場合は `wss://sora.sora-labo.shiguredo.app/signaling` を指定してください- `VITE_SORA_SIGNALING_URL=wss://sora.sora-labo.shiguredo.app/signaling`
- Sora Cloud の場合は `wss://sora.sora-cloud.shiguredo.app/signaling` を指定してください- `VITE_SORA_SIGNALING_URL=wss://sora.sora-cloud.shiguredo.app/signaling`

### VITE_SORA_CHANNEL_ID

好きな文字列を指定してください。

### VITE_SORA_CHANNEL_ID_PREFIX

**Sora Labo はこちらの環境変数を設定してください**

Sora のチャネル ID プレフィックスを指定してください。

`{github_username}_{github_id}_` を指定してください。

例えば github_username が spam で github_id が 1234567890 の場合、
チャネル ID プレフィックスは `spam_1234567890_` となります

```bash
# 例
# VITE_SORA_CHANNEL_ID_PREFIX=spam_1234567890_
VITE_SORA_CHANNEL_ID_PREFIX={github_username}_{github_id}_
```

### VITE_SORA_CHANNEL_ID_SUFFIX

**Sora Labo はこちらの環境変数を設定してください**

Sora Cloud のチャネル ID サフィックスを指定してください。

- `@{project_id}` を指定してください- `VITE_SORA_CHANNEL_ID_SUFFIX=@{project_id}`

### VITE_SECRET_KEY

**Sora Labo と Sora Cloud はこちらの環境変数を指定してください**

JWT 生成用のシークレットキーを指定してください。

- Sora Labo の場合はシークレットキーを指定してください- `VITE_SECRET_KEY={secret_key}`
- Sora Cloud の場合は API キーを指定してください- `VITE_SECRET_KEY={api_key}`

### vite-env.d.ts



```typescript
/// <reference types="vite/client" />

interface ImportMetaEnv {
  VITE_SORA_SIGNALING_URL: string;
  VITE_SORA_CHANNEL_ID: string;
  VITE_SORA_CHANNEL_ID_PREFIX: string;
  VITE_SORA_CHANNEL_ID_SUFFIX: string;
  VITE_SECRET_KEY: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
```

## jose

[Sora Labo](https://sora-labo.shiguredo.app/) や [Sora Cloud](https://sora-cloud.shiguredo.jp/) では接続に JWT を利用しています。

このドキュメントでは JWT を生成するのに [jose](https://github.com/panva/jose) というライブラリを利用しています。

### インストール

```console
# npm
$ npm install jose
```

```console
# pnpm
$ pnpm add jose
```

### JWT 生成するコード

```typescript
// jose をインストールしていることを前提としています
import { SignJWT } from "jose";

// channel_id と secret_key を指定して JWT を生成する
export const generateJwt = async (channelId: string, secretKey: string): Promise<string> => {
  // HS256 を利用しています
  return (
    new SignJWT({
      // channel_id を Claim に設定しています
      channel_id: channelId,
    })
      // JWT のヘッダーを設定しています
      .setProtectedHeader({ alg: "HS256", typ: "JWT" })
      // JWT の期限を 30 秒に設定しています
      .setExpirationTime("30s")
      // JWT を生成しています
      .sign(new TextEncoder().encode(secretKey))
  );
};
```

## チャネル ID の生成

環境変数を利用してチャネル ID を生成します。

```typescript
export const generateChannelId = (): string => {
  // qs を確認する
  const urlParams = new URLSearchParams(window.location.search);
  const qsChannelId = urlParams.get("channelId") || "";
  const qsChannelIdPrefix = urlParams.get("channelIdPrefix") || "";
  const qsChannelIdSuffix = urlParams.get("channelIdSuffix") || "";

  // qs が指定されていればその値を優先するようにする
  const channelId = qsChannelId || import.meta.env.VITE_SORA_CHANNEL_ID || "";
  const channelIdPrefix = qsChannelIdPrefix || import.meta.env.VITE_SORA_CHANNEL_ID_PREFIX || "";
  const channelIdSuffix = qsChannelIdSuffix || import.meta.env.VITE_SORA_CHANNEL_ID_SUFFIX || "";

  // 環境変数の channelId が指定されていない場合はエラー
  if (!channelId) {
    throw new Error("VITE_SORA_CHANNEL_ID is not set");
  }

  // channelIdPrefix と channelIdSuffix が指定されている場合はそれを利用する
  if (channelIdPrefix && channelIdSuffix) {
    return `${channelIdPrefix}${channelId}${channelIdSuffix}`;
  }

  // channelIdPrefix が指定されている場合はそれを利用する
  if (channelIdPrefix) {
    return `${channelIdPrefix}${channelId}`;
  }

  // channelIdSuffix が指定されている場合はそれを利用する
  if (channelIdSuffix) {
    return `${channelId}${channelIdSuffix}`;
  }

  return channelId;
};
```

## 送受信で接続する

自分から音声、映像を配信し、他の参加者から音声、映像を受信する場合は [sendrecv()](https://shiguredo.github.io/sora-js-sdk/interfaces/SoraConnection.html#sendrecv) を使用して接続します。

マルチストリームで接続した [Connection オブジェクト](https://shiguredo.github.io/sora-js-sdk/interfaces/ConnectionBase.html) は接続したチャネル ID に MediaStream が追加、削除されると track コールバックが呼ばれます。
コールバックを利用して追加、削除された MediaStream を処理します。

```html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
  </head>

  <body>
    <video id="localVideo" autoplay="" playsinline="" controls="" muted=""></video>
    <hr />
    <div id="remoteVideos"></div>

    <button id="connect">接続</button>
    <button id="disconnect">切断</button>

    <script type="module" src="./main.ts"></script>
  </body>
</html>
```

```typescript
import Sora, { type SoraConnection, type ConnectionPublisher } from "sora-js-sdk";
import { generateJwt } from "../jwt";
import { generateChannelId } from "../sora";

document.addEventListener("DOMContentLoaded", (_event) => {
  // Vite を利用して env.local から取得
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const secretKey = import.meta.env.VITE_SECRET_KEY;

  const channelId = generateChannelId();

  const soraClient = new SoraClient(signalingUrl, channelId, secretKey);
  document.querySelector("#connect")?.addEventListener("click", async () => {
    await soraClient.connect();
  });
  document.querySelector("#disconnect")?.addEventListener("click", async () => {
    await soraClient.disconnect();
  });
});

class SoraClient {
  private debug = true;

  private signalingUrl: string;
  private channelId: string;

  private secretKey: string;

  private sora: SoraConnection;
  private connection: ConnectionPublisher;

  private streams: Record<string, MediaStream> = {};

  constructor(signalingUrl: string, channelId: string, secretKey: string) {
    this.signalingUrl = signalingUrl;
    this.channelId = channelId;
    this.secretKey = secretKey;

    // 接続先の Sora を設定する
    this.sora = Sora.connection(this.signalingUrl, this.debug);
    const options = {
      audio: true,
      video: true,
    };
    this.connection = this.sora.sendrecv(this.channelId, undefined, options);

    // ontrack イベント
    // メディアストリームトラック単位で発火する
    this.connection.on("track", this.onaddtrack.bind(this));

    // removetrack イベント (リモートメディアストリームが削除されたときに発生)
    this.connection.on("removetrack", this.onremovetrack.bind(this));
  }

  async connect() {
    // オーディオとビデオのストリームを取得
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: true,
    });

    const localVideElement = document.querySelector<HTMLVideoElement>("#localVideo");
    if (localVideElement !== null) {
      localVideElement.srcObject = stream;
    }

    // SecretKey があれば JWT を設定する
    if (this.secretKey) {
      const jwt = await generateJwt(this.channelId, this.secretKey);
      this.connection.metadata = {
        // Sora では特に "access_token" と決まっているわけではありません
        // access_token は Sora Labo や Sora Cloud の想定です
        access_token: jwt,
      };
    }

    // 接続
    await this.connection.connect(stream);
  }

  async disconnect() {
    // 切断
    await this.connection.disconnect();

    // ローカルビデオを削除
    const localVideo = document.querySelector<HTMLVideoElement>("#localVideo");
    if (localVideo !== null) {
      localVideo.srcObject = null;
    }

    // リモートビデオを削除
    const remoteVideos = document.querySelector<HTMLDivElement>("#remoteVideos");
    remoteVideos?.remove();
  }

  // 統計情報を取得できるようにする
  async getStats(): Promise<RTCStatsReport | null> {
    if (!this.connection.pc) {
      return null;
    }

    return this.connection.pc.getStats();
  }

  private onaddtrack(event: RTCTrackEvent) {
    // 追加されたストリームを取得
    // 注: Sora では 1 クライアント 1 音声/ 1 映像と決まっているため、
    // ストリームが複数入ってこない
    const remoteStream = event.streams[0];

    // リモートビデオエレメントを取得
    const remoteVideos = document.querySelector("#remoteVideos");

    // リモートビデオエレメントのIDを生成
    const remoteVideoId = `remoteVideo-${remoteStream.id}`;

    // 既存のビデオエレメントが無ければ新たに作成
    if (!remoteVideos?.querySelector(`#${remoteVideoId}`)) {
      const remoteVideo = document.createElement("video");
      remoteVideo.id = remoteVideoId;
      remoteVideo.autoplay = true;
      remoteVideo.srcObject = remoteStream;
      remoteVideos?.appendChild(remoteVideo);
    }

    if (!this.streams[remoteStream.id]) {
      this.streams[remoteStream.id] = remoteStream;
    }
  }

  private onremovetrack(event: MediaStreamTrackEvent) {
    // target は removetrack が発火した MediaStream
    const target = event.target as MediaStream;
    const remoteVideo = document.querySelector(`#remoteVideo-${target.id}`);
    const remoteVideos = document.querySelector("#remoteVideos");
    if (remoteVideo) {
      remoteVideos?.removeChild(remoteVideo);
    }

    if (this.streams[target.id]) {
      delete this.streams[target.id];
    }
  }

  get getStreams(): Record<string, MediaStream> {
    return this.streams;
  }
}
```

## 送信のみで接続する

[sendonly()](https://shiguredo.github.io/sora-js-sdk/interfaces/SoraConnection.html#sendonly) を使用して接続します。

```html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
  </head>

  <body>
    <video id="localVideo" autoplay="" playsinline="" controls="" muted=""></video>
    <hr />

    <button id="connect">接続</button>
    <button id="disconnect">切断</button>

    <script type="module" src="./main.ts"></script>
  </body>
</html>
```

```typescript
import Sora, { type SoraConnection, type ConnectionPublisher } from "sora-js-sdk";
import { generateJwt } from "../jwt";
import { generateChannelId } from "../sora";

document.addEventListener("DOMContentLoaded", (_event) => {
  // Vite を利用して env.local から取得
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const secretKey = import.meta.env.VITE_SECRET_KEY;

  const channelId = generateChannelId();

  const soraClient = new SoraClient(signalingUrl, channelId, secretKey);
  document.querySelector("#connect")?.addEventListener("click", async () => {
    await soraClient.connect();
  });
  document.querySelector("#disconnect")?.addEventListener("click", async () => {
    await soraClient.disconnect();
  });
});

class SoraClient {
  private debug = true;

  private signalingUrl: string;
  private channelId: string;

  private secretKey: string;

  private sora: SoraConnection;
  private connection: ConnectionPublisher;

  constructor(signalingUrl: string, channelId: string, secretKey: string) {
    this.signalingUrl = signalingUrl;
    this.channelId = channelId;
    this.secretKey = secretKey;

    // 接続先の Sora を設定する
    this.sora = Sora.connection(this.signalingUrl, this.debug);
    const options = {};
    this.connection = this.sora.sendonly(this.channelId, undefined, options);

    // ontrack イベント
    // メディアストリームトラック単位で発火する
    this.connection.on("track", this.onaddtrack.bind(this));

    // removetrack イベント (リモートメディアストリームが削除されたときに発生)
    this.connection.on("removetrack", this.onremovetrack.bind(this));
  }

  async connect() {
    // オーディオとビデオのストリームを取得
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: true,
    });

    const localVideElement = document.querySelector<HTMLVideoElement>("#localVideo");
    if (localVideElement !== null) {
      localVideElement.srcObject = stream;
    }

    // SecretKey があれば JWT を設定する
    if (this.secretKey) {
      const accessToken = await generateJwt(this.channelId, this.secretKey);
      this.connection.metadata = {
        access_token: accessToken,
      };
    }

    // 接続
    await this.connection.connect(stream);
  }

  async disconnect() {
    // 切断
    await this.connection.disconnect();

    // ローカルビデオを削除
    const localVideElement = document.querySelector<HTMLVideoElement>("#localVideo");
    if (localVideElement !== null) {
      localVideElement.srcObject = null;
    }
  }

  private onaddtrack(event: RTCTrackEvent) {
    // 追加されたストリームを取得
    // 注: Sora では 1 クライアント 1 音声/ 1 映像と決まっているため、
    // ストリームが複数入ってこない
    const remoteStream = event.streams[0];

    // リモートビデオエレメントを取得
    const remoteVideos = document.querySelector("#remoteVideos");

    // リモートビデオエレメントのIDを生成
    const remoteVideoId = `remoteVideo-${remoteStream.id}`;

    // 既存のビデオエレメントが無ければ新たに作成
    if (!remoteVideos?.querySelector(`#${remoteVideoId}`)) {
      const remoteVideo = document.createElement("video");
      remoteVideo.id = remoteVideoId;
      remoteVideo.autoplay = true;
      remoteVideo.srcObject = remoteStream;
      remoteVideos?.appendChild(remoteVideo);
    }
  }

  private onremovetrack(event: MediaStreamTrackEvent) {
    // target は removetrack が発火した MediaStream
    const target = event.target as MediaStream;
    const remoteVideo = document.querySelector(`#remoteVideo-${target.id}`);
    const remoteVideos = document.querySelector("#remoteVideos");
    if (remoteVideo) {
      remoteVideos?.removeChild(remoteVideo);
    }
  }
}
```

## 受信のみで接続する

[recvonly()](https://shiguredo.github.io/sora-js-sdk/interfaces/SoraConnection.html#recvonly) を使用して接続します。

```html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
  </head>

  <body>
    <button id="connect">Connect</button>
    <button id="disconnect">Disconnect</button>

    <div id="remote-videos"></div>
    <script type="module" src="./main.ts"></script>
  </body>
</html>
```

```typescript
import Sora, {
  type SoraConnection,
  type ConnectionSubscriber,
  type ConnectionOptions,
} from "sora-js-sdk";
import { generateJwt } from "../jwt";
import { generateChannelId } from "../sora";

document.addEventListener("DOMContentLoaded", () => {
  // Vite を利用して env.local から取得
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const secretKey = import.meta.env.VITE_SECRET_KEY;

  const channelId = generateChannelId();

  const soraClient = new SoraClient(signalingUrl, channelId, secretKey);

  document.querySelector("#connect")?.addEventListener("click", async () => {
    await soraClient.connect();
  });
  document.querySelector("#disconnect")?.addEventListener("click", async () => {
    await soraClient.disconnect();
  });
});

class SoraClient {
  private debug = false;

  private channelId: string;
  private options: ConnectionOptions;
  private secretKey: string;

  private sora: SoraConnection;
  private connection: ConnectionSubscriber;

  private streams: Record<string, MediaStream> = {};

  constructor(
    signalingUrl: string,
    channelId: string,
    secretKey: string,
    options: ConnectionOptions = {},
  ) {
    this.channelId = channelId;
    this.secretKey = secretKey;
    this.options = options;

    // 接続先の Sora を設定する
    this.sora = Sora.connection(signalingUrl, this.debug);
    this.connection = this.sora.recvonly(this.channelId, undefined, this.options);
    // ontrack イベント
    // メディアストリームトラック単位で発火する
    this.connection.on("track", this.onaddtrack.bind(this));
    // removetrack イベント (リモートメディアストリームが削除されたときに発生)
    this.connection.on("removetrack", this.onremovetrack.bind(this));
  }

  async connect() {
    // SecretKey があれば JWT を設定する
    if (this.secretKey) {
      const jwt = await generateJwt(this.channelId, this.secretKey);
      this.connection.metadata = {
        // Sora では特に "access_token" と決まっているわけではありません
        // access_token は Sora Labo や Sora Cloud の想定です
        access_token: jwt,
      };
    }

    // 接続
    await this.connection.connect();
  }

  async disconnect() {
    // 切断
    await this.connection.disconnect();

    // リモートビデオを全て削除
    const remoteVideos = document.querySelector("#remote-videos");
    if (remoteVideos) {
      remoteVideos.innerHTML = "";
    }
  }

  async getStats(): Promise<RTCStatsReport | null> {
    if (!this.connection.pc) {
      throw new Error("missing pc");
    }

    return this.connection.pc.getStats();
  }

  private onaddtrack(event: RTCTrackEvent) {
    // 追加されたストリームを取得
    // 注: Sora では 1 クライアント 1 音声/ 1 映像と決まっているため、
    // ストリームが複数入ってこない
    const remoteStream = event.streams[0];

    // リモートビデオエレメントを取得
    const remoteVideos = document.querySelector("#remote-videos");

    // リモートビデオエレメントのIDを生成
    const remoteVideoId = `remote-video-${remoteStream.id}`;

    // 既存のビデオエレメントが無ければ新たに作成
    if (!remoteVideos?.querySelector(`#${remoteVideoId}`)) {
      const remoteVideo = document.createElement("video");
      remoteVideo.id = remoteVideoId;
      remoteVideo.autoplay = true;
      remoteVideo.srcObject = remoteStream;
      remoteVideos?.appendChild(remoteVideo);
    }

    if (!this.streams[remoteStream.id]) {
      this.streams[remoteStream.id] = remoteStream;
    }
  }

  private onremovetrack(event: MediaStreamTrackEvent) {
    // target は removetrack が発火した MediaStream
    const target = event.target as MediaStream;
    const remoteVideo = document.querySelector(`#remote-video-${target.id}`);
    const remoteVideos = document.querySelector("#remote-videos");
    if (remoteVideo) {
      remoteVideos?.removeChild(remoteVideo);
    }

    if (this.streams[target.id]) {
      delete this.streams[target.id];
    }
  }

  get getStreams(): Record<string, MediaStream> {
    return this.streams;
  }
}
```

## 接続して 5 秒後に切断する

[disconnect()](https://shiguredo.github.io/sora-js-sdk/interfaces/ConnectionBase.html#disconnect) を使用して接続を切断します。

```typescript
import Sora from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // 接続先の Sora を設定する
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const sora = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {};
  const client = sora.sendonly(channelId, undefined, options);

  // secretKey が指定されている場合は JWT を生成する
  const secretKey = import.meta.env.VITE_SECRET_KEY || "";
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    client.metadata = {
      access_token: jwt,
    };
  }

  // オーディオとビデオのストリームを取得
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
  await client.connect(stream);

  // 5 秒後に切断する
  setTimeout(async () => {
    await client.disconnect();
  }, 5000);
};

export default connectToSora;
```

## サイマルキャストを有効にして接続する

サイマルキャスト (Simulcast) は配信時に 1 つの RTCPeerConnection から複数種類のエンコードした映像を配信する機能です。
詳しくは Sora ドキュメント [サイマルキャスト](https://sora-doc.shiguredo.jp/SIMULCAST) をご確認ください。

[ConnectionBase オプション](https://shiguredo.github.io/sora-js-sdk/interfaces/ConnectionBase.html#options) に `simulcast: true` を指定します

```typescript
import Sora, { type VideoCodecType } from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // 接続先の Sora を設定する
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const sora = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {
    simulcast: true,
    videoCodecType: "VP8" as VideoCodecType,
  };
  const sendonly = sora.sendonly(channelId, undefined, options);

  const secretKey = import.meta.env.VITE_SECRET_KEY || "";
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    sendonly.metadata = {
      access_token: jwt,
    };
  }

  // オーディオとビデオのストリームを取得
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });

  await sendonly.connect(stream);
};

export default connectToSora;
```

## スポットライトで接続する

スポットライト (Spotlight) は一定の音量を超えて音声を発している参加者の場合は音声や高画質映像を、それ以外の参加者は音声のない低画質映像を配信する機能です。
詳しくは Sora ドキュメント [スポットライト](https://sora-doc.shiguredo.jp/SPOTLIGHT) をご確認ください。

[ConnectionBase オプション](https://shiguredo.github.io/sora-js-sdk/interfaces/ConnectionBase.html#options)  に `spotlight: true` を指定します

```typescript
import Sora, { type VideoCodecType } from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // 接続先の Sora を設定する
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const sora = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {
    simulcast: true,
    // スポットライト機能を有効にする
    spotlight: true,
    videoCodecType: "VP8" as VideoCodecType,
  };
  const client = sora.sendonly(channelId, undefined, options);

  // シークレットキーが設定されている場合は JWT を設定する
  const secretKey = import.meta.env.VITE_SECRET_KEY || "";
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    client.metadata = {
      access_token: jwt,
    };
  }

  // オーディオとビデオのストリームを取得
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
  await client.connect(stream);
};

export default connectToSora;
```

## 音声や映像コーデックを指定する

音声や映像のコーデックタイプは [ConnectionBase オプション](https://shiguredo.github.io/sora-js-sdk/interfaces/ConnectionBase.html#options) で指定します

```typescript
import Sora, { type AudioCodecType, type VideoCodecType } from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // 接続先の Sora を設定する
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const soraConnection = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {
    // 音声コーデックに Opus を指定
    audioCodecType: "OPUS" as AudioCodecType,
    // 映像コーデックに VP9 を指定
    videoCodecType: "VP9" as VideoCodecType,
  };
  const sendrecv = soraConnection.sendrecv(channelId, undefined, options);

  const secretKey = import.meta.env.VITE_SECRET_KEY || "";
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    sendrecv.metadata = {
      access_token: jwt,
    };
  }

  // オーディオとビデオのストリームを取得
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
  await sendrecv.connect(stream);
};

export default connectToSora;
```

- [音声コーデックタイプ](https://shiguredo.github.io/sora-js-sdk/types/AudioCodecType.html)
- [映像コーデックタイプ](https://shiguredo.github.io/sora-js-sdk/types/VideoCodecType.html)

## 音声や映像のビットレートを指定する

**音声のビットレート指定は推奨しません**

音声や映像のビットレートは [ConnectionBase オプション](https://shiguredo.github.io/sora-js-sdk/interfaces/ConnectionBase.html#options) で指定します

```typescript
import Sora from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // Sora への接続を作成
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const soraConnection = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {
    audio: true,
    // オーディオのビットレートを 64 kbps に設定
    audioBitRate: 64,
    video: true,
    // ビデオのビットレートを 192 kbps に設定
    videoBitRate: 192,
  };
  const sendrecv = soraConnection.sendrecv(channelId, undefined, options);

  const secretKey = import.meta.env.VITE_SECRET_KEY || "";
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    sendrecv.metadata = {
      access_token: jwt,
    };
  }

  // オーディオとビデオのストリームを取得
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
  await sendrecv.connect(stream);
};

export default connectToSora;
```

- 音声ビットレートに指定できる範囲は 6-510 です
- 映像ビットレートに指定できる範囲は 1-30000 です

## 映像コーデックパラメーターを指定する

[ConnectionBase オプション](https://shiguredo.github.io/sora-js-sdk/interfaces/ConnectionBase.html#options) で指定します。

```typescript
import Sora, { type VideoCodecType } from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // 接続先の Sora を設定する
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const soraConnection = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {
    video: true,
    videoCodecType: "VP9" as VideoCodecType,
    // VP9 の 映像パラメーターで profile-id を 2 に指定
    // これを利用するには sora.conf にて signaling_vp9_params = true を設定する必要がある
    videoVp9Params: {
      profileId: 2,
    },
  };
  const sendonly = soraConnection.sendonly(channelId, undefined, options);

  // secretKey が設定されている場合は JWT を設定する
  const secretKey = import.meta.env.VITE_SECRET_KEY;
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    sendonly.metadata = {
      access_token: jwt,
    };
  }

  // オーディオとビデオのストリームを取得
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
  await sendonly.connect(stream);
};

export default connectToSora;
```

AV1 や H.264 や H.265 でも指定可能です。

## 映像と音声の可否を指定する

[ConnectionBase オプション](https://shiguredo.github.io/sora-js-sdk/interfaces/ConnectionBase.html#options) で指定します。

音声なし

```typescript
import Sora from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // Sora への接続を作成
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const soraConnection = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {
    // オーディオは不要
    audio: false,
  };
  const sendrecv = soraConnection.sendrecv(channelId, undefined, options);

  const secretKey = import.meta.env.VITE_SECRET_KEY;
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    sendrecv.metadata = {
      access_token: jwt,
    };
  }

  // ユーザーメディア（ビデオのみ）を取得
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: false,
    video: true,
  });
  await sendrecv.connect(stream);
};

export default connectToSora;
```

映像なし

```typescript
import Sora from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // ユーザーメディア（オーディオのみ）を取得
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: false,
  });

  // 接続先の Sora を設定する
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const soraConnection = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {
    // ビデオは不要
    video: false,
  };
  const sendonly = soraConnection.sendonly(channelId, undefined, options);

  // シークレットキーが設定されている場合は JWT を設定する
  const secretKey = import.meta.env.VITE_SECRET_KEY;
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    sendonly.metadata = {
      access_token: jwt,
    };
  }

  await sendonly.connect(stream);
};

export default connectToSora;
```

## クライアントIDを指定する

接続時やサーバー認証成功時に任意の文字列を指定できる値です。
詳しくは [Sora ドキュメント WebSocket 経由のシグナリング client_id](https://sora-doc.shiguredo.jp/SIGNALING#cbaf08) を参照してください。

[ConnectionBase オプション](https://shiguredo.github.io/sora-js-sdk/interfaces/ConnectionBase.html#options) で指定します。

```typescript
import Sora from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // Sora への接続を作成
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const soraConnection = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {
    // client-id に xyz を指定
    // client-id は重複可能
    clientId: "xyz",
  };
  const sendrecv = soraConnection.sendrecv(channelId, undefined, options);

  const secretKey = import.meta.env.VITE_SECRET_KEY || "";
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    sendrecv.metadata = {
      access_token: jwt,
    };
  }

  // オーディオとビデオのストリームを取得
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
  await sendrecv.connect(stream);
};

export default connectToSora;
```

## DataChannel 経由のシグナリングを使用する

WebRTC 接続確立後に、シグナリングを WebSocket 経由から DataChannel 経由に切り替える機能です。

DataChannel 経由のシグナリングを利用するには、接続時に `dataChannelSignaling: true` を指定するか、認証成功時に `data_channel_signaling` を `true` を設定するか、 `sora.conf` にて `default_data_channel_signaling` を `true` に設定する必要があります。

詳しくは [Sora ドキュメント DataChannel 経由のシグナリング](https://sora-doc.shiguredo.jp/DATA_CHANNEL_SIGNALING) を参照してください。

[ConnectionBase オプション](https://shiguredo.github.io/sora-js-sdk/interfaces/ConnectionBase.html#options) で指定します。

```typescript
import Sora, { type SignalingSwitchedMessage } from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // 接続先の Sora を設定する
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const sora = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {
    // シグナリングを WebSocket 経由から DataChannel 経由に切り替えるかどうかを指定
    dataChannelSignaling: true,
    // シグナリングを DataChannel 経由に切り替えたあとに WebSocket を切断するかどうかを指定
    // true にした場合、 Sora JS SDK は自動で WebSocket を切断します
    ignoreDisconnectWebSocket: false,
  };
  const client = sora.sendrecv(channelId, undefined, options);

  client.on("switched", (event: SignalingSwitchedMessage) => {
    console.log("Switched signaling to DataChannel:", event);
  });

  const secretKey = import.meta.env.VITE_SECRET_KEY || "";
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    client.metadata = {
      access_token: jwt,
    };
  }

  const mediaStream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
  await client.connect(mediaStream);
};

export default connectToSora;
```

## メッセージング機能を使用する

DataChannel を利用したデータの送受信を行える機能です。
詳しくは Sora ドキュメントの [リアルタイムメッセージング機能](https://sora-doc.shiguredo.jp/MESSAGING) をご確認ください。

[ConnectionBase オプション](https://shiguredo.github.io/sora-js-sdk/interfaces/ConnectionBase.html#options) で指定します。

```typescript
import Sora, {
  type DataChannelDirection,
  type SignalingNotifyConnectionCreated,
  type SignalingSwitchedMessage,
} from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // 接続先の Sora を設定する
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const sora = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {
    // メッセージング機能を利用するには、データチャネルを利用したシグナリングを有効にする必要がある
    dataChannelSignaling: true,
    dataChannels: [
      {
        // メッセージングのラベルは # から始める必要がある
        label: "#example",
        // メッセージングの方向、sendrecv は送受信可能
        // sendonly の場合は送信のみ可能
        // recvonly の場合は受信のみ可能
        direction: "sendrecv" as DataChannelDirection,
        // https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/maxPacketLifeTime
        // https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/maxRetransmits
        // maxPacketLifeTime か maxResends のどちらかしか指定できない
        maxPacketLifeTime: 60_000,

        // https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/ordered
        ordered: true,
      },
    ],
  };
  const sendrecv = sora.sendrecv(channelId, undefined, options);

  const secretKey = import.meta.env.VITE_SECRET_KEY || "";
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    sendrecv.metadata = {
      access_token: jwt,
    };
  }

  // connected コールバック（自分の接続確立時に 1 度だけ発火）
  sendrecv.on("connected", (event: SignalingNotifyConnectionCreated) => {
    console.log("connected");
    console.log("self-connection_id:", event.connection_id);
  });

  // switched コールバック（DataChannel シグナリングへの切り替え完了時に発火）
  sendrecv.on("switched", (event: SignalingSwitchedMessage) => {
    console.log("switched");
    console.log("ignore_disconnect_websocket:", event.ignore_disconnect_websocket);
  });

  // メッセージ送信が可能な datachannel との接続が確立した場合に on datachannel イベントが発火する
  sendrecv.on("datachannel", async (event) => {
    // event.datachannel にメッセージ送信可能な datachannel の情報が含まれる
    const label = event.datachannel.label; // #example
    console.log("ondatachannel", label);
    // メッセージを送信する
    await sendrecv.sendMessage(label, new TextEncoder().encode("Hello world."));
  });

  // メッセージを受信した際に on message イベントが発火する
  sendrecv.on("message", (event) => {
    const label = event.label;
    const data = event.data;
    console.log("onmessage", label, data);
  });

  const mediaStream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
  await sendrecv.connect(mediaStream);

  console.log("Connected to Sora");
};

// Soraへの接続とメッセージ送受信を開始
export default connectToSora;
```

メッセージングのみで接続することも可能です。

```typescript
import Sora, {
  type DataChannelDirection,
  type SignalingNotifyConnectionCreated,
  type SignalingSwitchedMessage,
} from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // 接続先の Sora を設定する
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const sora = Sora.connection(signalingUrl, debug);

  // ConnectionOptions の dataChannels を指定する
  const channelId = generateChannelId();
  const options = {
    // データチャネルメッセージオンリーを使う場合は設定が必要
    // https://sora-doc.shiguredo.jp/MESSAGING#3a3616
    audio: false,
    video: false,
    // DataChannel シグナリングを利用する
    dataChannelSignaling: true,
    // データチャネルメッセージングの定義を追加
    dataChannels: [
      {
        label: "#example",
        direction: "sendrecv" as DataChannelDirection,
      },
    ],
  };
  // Sora 2023.2.0 から role が sendrecv 以外でもメッセージングオンリーが利用可能になった
  const messaging = sora.messaging(channelId, undefined, options);

  const secretKey = import.meta.env.VITE_SECRET_KEY || "";
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    messaging.metadata = {
      access_token: jwt,
    };
  }

  // connected コールバック（自分の接続確立時に 1 度だけ発火）
  messaging.on("connected", (event: SignalingNotifyConnectionCreated) => {
    console.log("connected");
    console.log("self-connection_id:", event.connection_id);
  });

  // switched コールバック（DataChannel シグナリングへの切り替え完了時に発火）
  messaging.on("switched", async (event: SignalingSwitchedMessage) => {
    console.log("switched");
    console.log("ignore_disconnect_websocket:", event.ignore_disconnect_websocket);
    // switched 後にメッセージ送信が可能
    await messaging.sendMessage("#example", new TextEncoder().encode("Hello world."));
  });

  await messaging.connect();
  // Sora 2023.2.0 以前のバージョンでは sendrecv で接続する必要がある
  // もし sendrecv や sendonly を利用する場合は空のメディアストリームを渡す
  // await sendrecv.connect(new MediaStream());
};

export default connectToSora;
```


## RPC 機能を使用する

DataChannel を利用した RPC 機能です。

RPC 機能を利用するには、DataChannel シグナリング機能を有効にし、さらに Sora 側出認証成功時に `rpc_methods` で利用できる RPC メソッド一覧を指定する必要があります。

詳しくは Sora ドキュメントの [RPC 機能](https://sora-doc.shiguredo.jp/RPC) をご確認ください。

[ConnectionBase オプション](https://shiguredo.github.io/sora-js-sdk/interfaces/ConnectionBase.html#options) で指定します。

```typescript
import Sora, { type RPCOptions, type SignalingPushMessage } from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // 接続先の Sora を設定する
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const sora = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {
    // RPC 機能を利用するには、データチャネルを利用したシグナリングを有効にする必要がある
    dataChannelSignaling: true,
  };
  const sendrecv = sora.sendrecv(channelId, undefined, options);

  const secretKey = import.meta.env.VITE_SECRET_KEY || "";
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    sendrecv.metadata = {
      access_token: jwt,
    };
  }

  // RPC のレスポンスを受信するために push イベントをハンドリングする
  sendrecv.on("push", (event: SignalingPushMessage) => {
    console.log("push", event.data);
  });

  const mediaStream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
  await sendrecv.connect(mediaStream);

  console.log("Connected to Sora");

  // RPC を送信する
  // 利用可能な RPC メソッドは認証成功時に rpc_methods として払い出される
  const rpcMethod = "2025.2.0/RequestSimulcastRid";
  const rpcParams = {
    rid: "r1",
  };
  const rpcOptions: RPCOptions = {
    // notification を true にするとレスポンスを待たない
    notification: true,
    // タイムアウト時間をミリ秒で指定 (オプション)
    timeout: 5000,
  };
  await sendrecv.rpc(rpcMethod, rpcParams, rpcOptions);

  console.log("RPC sent successfully");
};

// Sora への接続と RPC 送信を開始
export default connectToSora;
```

## シグナリング通知機能を使用する

シグナリング通知機能は自分の接続状況や他の接続の参加や離脱などの通知する仕組みです。

詳しくは Sora ドキュメント の [シグナリング通知](https://sora-doc.shiguredo.jp/SIGNALING_NOTIFY) をご確認ください。

```typescript
import Sora, { type SignalingNotifyConnectionCreated } from "sora-js-sdk";
import { generateJwt } from "./jwt";
import { generateChannelId } from "./sora";

const connectToSora = async () => {
  // 接続先の Sora を設定する
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  const debug = true;
  const sora = Sora.connection(signalingUrl, debug);

  const channelId = generateChannelId();
  const options = {};
  const sendonly = sora.sendonly(channelId, undefined, options);

  const secretKey = import.meta.env.VITE_SECRET_KEY || "";
  if (secretKey) {
    const jwt = await generateJwt(channelId, secretKey);
    sendonly.metadata = {
      access_token: jwt,
    };
  }

  // connected コールバック（自分の接続確立時に 1 度だけ発火）
  sendonly.on("connected", (event: SignalingNotifyConnectionCreated) => {
    console.log("connected");
    console.log("self-connection_id:", event.connection_id);
  });

  // オーディオとビデオのストリームを取得
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
  await sendonly.connect(stream);
};

export default connectToSora;
```


