チュートリアル¶
本章では 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 について¶
jose は Sora Labo や Sora 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 ボタン
を押して、双方向で配信ができていれば成功です。