チュートリアル

本章では Sora JavaScript SDK を使って音声と映像を送受信できる簡単なサンプルを作成します。

プロジェクトの作成

開発環境ツールとして Vite を利用します。 無理に Vite を利用する必要は無く、慣れたツールを利用してください。

パッケージマネージャーとしては pnpm を利用していますが、 npm や yarn でも問題ありません。

$ pnpm create vite@latest
✔ Project name: … sora-js-sdk-tutorial
✔ Select a framework: › Vanilla
✔ Select a variant: › TypeScript

Scaffolding project in /private/tmp/sora-js-sdk-tutorial...

Done. Now run:

  cd sora-js-sdk-tutorial
  pnpm install
  pnpm run dev
tree
.
├── index.html
├── package.json
├── public
│   └── vite.svg
├── src
│   ├── counter.ts
│   ├── main.ts
│   ├── style.css
│   ├── typescript.svg
│   └── vite-env.d.ts
└── tsconfig.json

sora-js-sdk の追加

$ pnpm add -E sora-js-sdk jose

jose について

joseSora LaboSora Cloud を利用する場合の JWT 生成に必要になります。

Vite と TypeScript を最新にする

$ pnpm up vite@latest typescript@latest

不要なファイルの削除

以下のファイルは利用しないため削除してください。

  • public/vite.svg

  • src/counter.ts

  • src/typescript.svg

  • src/style.css

index.html の変更

  • connect は接続ボタン

  • disconnect は切断ボタン

  • local-video は自分が取得した映像を出力する

  • remote-videos は他の映像を表示する

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" href="data:,">
  <title>Sora JS SDK Tutorial</title>
</head>

<body>
  <p>
    <button id="connect">Connect</button>
    <button id="disconnect" disabled>Disconnect</button>
  </p>
  <video id="local-video" autoplay playsInline controls muted style="transform: scaleX(-1)"></video>
  <div id="remote-videos"></div>
  <script type="module" src="./src/main.ts"></script>
</body>

</html>

.env.local の作成

.env.local ファイルを作成してください。

$ touch .env.local

環境変数を設定してください。

Sora を自前で立てる場合

# Sora のシグナリング URL
VITE_SORA_SIGNALING_URL=wss://{host}/signaling
# 好きな文字列
VITE_SORA_CHANNEL_ID=tutorial

Sora Cloud

# Sora Cloud のシグナリング URL
VITE_SORA_SIGNALING_URL=wss://sora.sora-cloud.shiguredo.app/signaling
# 好きな文字列
VITE_SORA_CHANNEL_ID=tutorial
# Sora Cloud のダッシュボードから取得できる @ + プロジェクト ID
VITE_SORA_CHANNEL_ID_SUFFIX=@{project_id}
# Sora Cloud のダッシュボードから取得できる API キー
VITE_SECRET_KEY={api_key}

Sora Labo

# Sora Labo のシグナリング URL
VITE_SORA_SIGNALING_URL=wss://sora.sora-labo.shiguredo.app/signaling
# 好きな文字列
VITE_SORA_CHANNEL_ID=tutorial
# Sora Labo のダッシュボードから取得できるチャネル ID プレフィックス
# {GitHub ユーザ名}_{GitHub ID}_
VITE_SORA_CHANNEL_ID_PREFIX={github_username}_{github_id}_
# Sora Labo のダッシュボードから取得できるシークレットキー
VITE_SECRET_KEY={secret_key}

src/client.ts の追加

Sora JavaScript SDK を利用したクライアント、 Sora Client クラスを作成します。

import { SignJWT } from "jose";
import Sora, {
  type ConnectionOptions,
  type SoraConnection,
  type ConnectionPublisher,
} from "sora-js-sdk";

// Sora JavaScript SDK を利用した Sora クライアント
class SoraClient {
  private debug: boolean;

  private channelId: string;
  private options: ConnectionOptions;

  private secretKey: string;

  private sora: SoraConnection;
  private connection: ConnectionPublisher;

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

    this.sora = Sora.connection(signalingUrl, this.debug);
    // metadata はここでは undefined にして connect 時に指定する
    this.connection = this.sora.sendrecv(this.channelId, undefined, this.options);
    this.connection.on("track", this.onTrack);
    this.connection.on("removetrack", this.removeTrack);
  }

  async connect(stream: MediaStream) {
    // SecretKey が指定されていたら JWT を生成して metadata に設定する
    if (this.secretKey) {
      const jwt = await this.generateJwt();
      this.connection.metadata = {
        access_token: jwt,
      };
    }
    // 接続する
    await this.connection.connect(stream);
  }

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

  private onTrack = (event: RTCTrackEvent) => {
    const stream = event.streams[0];
    const remoteVideoId = `remote-video-${stream.id}`;
    const remoteVideos = document.querySelector<HTMLDivElement>("#remote-videos");
    if (remoteVideos && !remoteVideos.querySelector<HTMLVideoElement>(`#${remoteVideoId}`)) {
      const remoteVideo = document.createElement("video");
      remoteVideo.id = remoteVideoId;
      remoteVideo.style.border = "1px solid red";
      remoteVideo.autoplay = true;
      remoteVideo.playsInline = true;
      remoteVideo.controls = true;
      remoteVideo.width = 320;
      remoteVideo.height = 240;
      remoteVideo.srcObject = stream;
      remoteVideos.appendChild(remoteVideo);
    }
  };

  private removeTrack = (event: MediaStreamTrackEvent) => {
    const target = event.target as MediaStream;
    const remoteVideo = document.getElementById(`remote-video-${target.id}`) as HTMLVideoElement;
    if (remoteVideo) {
      remoteVideo.remove();
    }
  };

  private generateJwt = (): Promise<string> => {
    return new SignJWT({
      channel_id: this.channelId,
    })
      .setProtectedHeader({ alg: "HS256", typ: "JWT" })
      .setExpirationTime("30s")
      .sign(new TextEncoder().encode(this.secretKey));
  };
}

export default SoraClient;

src/vite-env.d.ts の変更

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

interface ImportMetaEnv {
  VITE_SORA_SIGNALING_URL: string;
  VITE_SORA_CHANNEL_ID_PREFIX_PREFIX: string;
  VITE_SECRET_KEY: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

src/main.ts の変更

Sora Client クラスを利用したコードを追加します。

import SoraClient from "./client";

addEventListener("DOMContentLoaded", () => {
  // .env.local からシグナリング URL を取得する
  const signalingUrl = import.meta.env.VITE_SORA_SIGNALING_URL;
  // .env.local からシークレットキーを取得する、無ければ空文字
  const secretKey = import.meta.env.VITE_SECRET_KEY || "";

  // チャネル ID を生成する
  const channelId = generateChannelId();

  // SoraClient を生成する
  const client = new SoraClient(signalingUrl, channelId, secretKey);

  // connect ボタンを押した時の処理
  document.getElementById("connect")?.addEventListener("click", async () => {
    // getUserMedia でカメラから映像を取得する
    const stream = await navigator.mediaDevices.getUserMedia({
      // 音声は無効
      audio: false,
      video: true,
    });

    await client.connect(stream);

    const localVideo = document.getElementById("local-video") as HTMLVideoElement;
    if (localVideo) {
      localVideo.srcObject = stream;
    }
  });

  document.getElementById("disconnect")?.addEventListener("click", async () => {
    if (client === undefined) {
      return;
    }

    // sendrecv があるかどうか確認する
    // 切断する
    await client.disconnect();
    const localVideo = document.getElementById("local-video") as HTMLVideoElement;
    if (localVideo) {
      localVideo.srcObject = null;
    }
    const remoteVideos = document.getElementById("remote-videos") as HTMLDivElement;
    while (remoteVideos?.firstChild) {
      remoteVideos.removeChild(remoteVideos.firstChild);
    }
  });
});

// 環境変数からチャネル ID を生成する
export const generateChannelId = (): string => {
  const channelId = import.meta.env.VITE_SORA_CHANNEL_ID || "";
  const channelIdPrefix = import.meta.env.VITE_SORA_CHANNEL_ID_PREFIX || "";
  const channelIdSuffix = 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;
};

起動

$ pnpm run dev
VITE v6.0.7  ready in 232 ms

➜  Local:   http://localhost:5173/
➜  Network: use --host to expose
➜  press h + enter to show help

http://localhost:5173/ へアクセスして、ブラウザのタブをふたつ以上開いて、 Connect ボタン を押して、双方向で配信ができていれば成功です。

© Copyright 2024, Shiguredo Inc. Created using Sphinx 8.1.3