🈳

Sora JavaScript SDKをNext.jsで使う

2024/01/07に公開

こちらのSora SFUのSDKをNext.jsで使ってみる。
https://github.com/shiguredo/sora-js-sdk

成果物

サンプルコードはこちら。

https://github.com/SatoshiKawabata/sora-nextjs-sample

Next.jsのプロジェクトの作成

まずNext.jsのプロジェクトを作成する。最初の設定はデフォの通り。

$ yarn create next-app
yarn create v1.22.21
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Installed "create-next-app@14.0.4" with binaries:
      - create-next-app
✔ What is your project named? … sora-sdk-nextjs
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes

sora-sdk-jsの導入

公式のチュートリアルはviteを使ったサンプルなのでこれを参考にNext.jsに組み込んでいく。
https://sora-js-sdk.shiguredo.jp/tutorial

$ yarn add sora-js-sdk

検証用にSora Labを使う

Sora Labo は、時雨堂が開発する WebRTC SFU Sora を無料で検証できるサービス。シグナリングサーバを無料で検証用に使える。GitHubアカウントなどでサインアップする。
https://sora-labo.shiguredo.app/home

以下の情報をSora Labから持ってくる。

  • シグナリングURL
  • チャネルID
  • シークレットキー

.env.local の作成

環境変数ファイルを追加。

$ touch .env.local

中身はこれ。Sora Labのページから持ってきた値を入れていく。

NEXT_PUBLIC_SORA_SIGNALING_URL={Sora LabのシグナリングURL}
NEXT_PUBLIC_SORA_CHANNEL_ID={Sora LabのチャネルID}
NEXT_PUBLIC_ACCESS_TOKEN={Sora Labのシークレットキー}

接続処理

WebRTCの接続のための重要なコードはこのあたり。

const soraConnection = Sora.connection(signalingUrl, debug);
const connection = soraConnection.sendrecv(channelId, metadata, options);
connection.on("track", (event) => {
    const stream = event.streams[0];
    // streamをvideoタグのsrcObjectにセットする
});
    await connection.connect(stream);

Reactのコード

上記コードをNext.jsに組み込むためにsrc/app/page.tsxのコードを書き換えた。yarn devで起動してブラウザを2つ開いて相互に接続できたので成功!

"use client";
import { useCallback, useEffect, useState } from "react";

import Sora from "sora-js-sdk";
import type { ConnectionPublisher } from "sora-js-sdk";

export default function Home() {
  const [remoteStreams, setRemoteStreams] = useState<MediaStream[]>([]);
  const [sendrecv, setSendrecv] = useState<ConnectionPublisher | null>(null);

  const handleConnect = async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: false,
        video: true,
      });
      const debug = true;
      const signalingUrl = process.env.NEXT_PUBLIC_SORA_SIGNALING_URL as string;
      const channelId = process.env.NEXT_PUBLIC_SORA_CHANNEL_ID as string;
      const metadata = {
        access_token: process.env.NEXT_PUBLIC_ACCESS_TOKEN,
      };
      const options = {
        multistream: true,
      };
      const soraConnection = Sora.connection(signalingUrl, debug);
      const connection = soraConnection.sendrecv(channelId, metadata, options);
      connection.on("track", (event) => {
        const stream = event.streams[0];
        setRemoteStreams((prevStreams) => [...prevStreams, stream]);
      });
      connection.on("removetrack", (event) => {
        const target = event.target as MediaStream;
        setRemoteStreams((prevStreams) =>
          prevStreams.filter((stream) => stream.id !== target.id)
        );
      });
      await connection.connect(stream);
      setSendrecv(connection);
    } catch (error) {
      console.error("Failed to connect:", error);
    }
  };

  const handleDisconnect = useCallback(async () => {
    if (sendrecv) {
      await sendrecv.disconnect();
      setRemoteStreams([]);
    }
  }, [sendrecv]);

  useEffect(() => {
    return () => {
      handleDisconnect();
    };
  }, [handleDisconnect]);

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <button
        id="connectButton"
        onClick={handleConnect}
        disabled={sendrecv !== null}
      >
        Connect
      </button>
      <button
        id="disconnectButton"
        onClick={handleDisconnect}
        disabled={sendrecv === null}
      >
        Disconnect
      </button>
      <div id="remoteVideos">
        {remoteStreams.map((stream) => (
          <video
            key={stream.id}
            id={`remoteVideo-${stream.id}`}
            autoPlay
            playsInline
            ref={(video) => video && (video.srcObject = stream)}
          />
        ))}
      </div>
    </main>
  );
}

Discussion