使い始める
Console でプロジェクト、リンク、キーを作り、リンクで接続してカメラとマイクを出し、他の全員のものを再生するブラウザーのページを書いて、二つのブラウザーで開きます。それから、アプリが相手が誰で何をしてよいかを決めるときのために、トークンに署名するバックエンドを足します。
ホストするサービスはまだ公開していません。 その Console と Relay はまだないので、この手順はまだそこでは進められません。自分のマシンで進める方法はホストするサービスの公開までにあります。
1. サインインしてプロジェクトを作る
Console のサインイン画面を開き、GitHub でサインインします。プロジェクトの画面で、名前を付けてプロジェクトを作ります。そのプロジェクトの画面に、プロジェクトの ID と、アプリが接続する Relay の URL が出ます。
2. リンクを作る
リンクは、その ID を持つ人を、バックエンドなしで、作ったときのルートと権利で接続させます。リンクの画面で、次のように作ります。
| 項目 | 値 | 意味 |
|---|---|---|
| ルート | 空欄 | プロジェクト全体。rooms/42 のようなパスにすると、リンクはその下に限られる |
| 出す(publish) | {id}/** |
各自は自分の ID の下にだけ出せる。{id} は入る人の ID を表す |
| 読む(subscribe) | ** |
全員がルートの下をすべて読める |
リンクの ID(lk_…)をコピーします。持っている人は誰でも、リンクを削除するまでこの権利で接続できるので、ページを開いてよい範囲にだけ渡してください。
3. リンクで接続するページ
SDK はパッケージ tablebox.io です。まだパッケージレジストリにはないので、Tablebox のリポジトリで npm run build(ワークスペース packages/sdk)を実行して作ります。
ブラウザーのサンプルは、Relay の URL を relay.ts から取ります。プロジェクトの画面に出る URL か、ホストするサービスの公開まではローカルスタックの Relay とその証明書の SHA-256 を書きます。
// The Relay URL; for a local Relay with a self-signed certificate, also its SHA-256 (Get started).
export const relay = { url: 'https://relay.example/', serverCertificateHashes: [] as string[] };// Get started: connect with the link in the page's address (page.html#link={id}), publish the
// camera and microphone at {identity}/camera.hang, and play everyone else's.
import { connect, moq, publish, watch } from 'tablebox.io';
import { relay } from './relay.ts';
const link = new URLSearchParams(location.hash.slice(1)).get('link') ?? '';
const connection = await connect({ ...relay, link });
const me = connection.identity!;
connection.status.subscribe(status => {
document.getElementById('status')!.textContent = `${me}: ${status} over ${connection.transport.peek() ?? '…'}`;
});
addEventListener('pagehide', () => connection.close());
const camera = new publish.Source.Camera();
const microphone = new publish.Source.Microphone();
const video = new publish.Video.Capture({ source: new publish.Signals.Computed(e => e.get(camera.out.source)?.video) });
const audio = new publish.Audio.Capture({ source: new publish.Signals.Computed(e => e.get(microphone.out.source)?.audio) });
const broadcast = new publish.Broadcast({ origin: connection.origin, name: moq.Path.from(`${me}/camera.hang`), display: video.out.display });
new publish.Video.Encoder('video/hd', { broadcast, capture: video });
new publish.Audio.Encoder('audio', { broadcast, capture: audio });
const people = new Map<string, { item: HTMLLIElement; player: watch.Player }>();
for await (const { kind, prefix } of connection.list('')) {
const who = prefix.slice(0, prefix.indexOf('/'));
if (!prefix.endsWith('/camera.hang') || who === me) continue;
if (kind === 'retracted') {
people.get(who)?.player.close();
people.get(who)?.item.remove();
people.delete(who);
} else if (!people.has(who)) {
const item = document.getElementById('people')!.appendChild(document.createElement('li'));
item.textContent = who;
const canvas = item.appendChild(document.createElement('canvas'));
people.set(who, { item, player: new watch.Player({ origin: connection.origin, name: prefix, canvas }) });
}
}ページは、ブラウザーの中にとどまるアドレスのフラグメントからリンクの ID を読みます。リンクでは、SDK がブラウザーごとに一度だけシークレットを作って保ち、Relay がそこから人の ID を導くので、再読み込みしても同じ人として接続します。ページは公式の publish の部品でカメラとマイクを {id}/camera.hang に出し、出ているものを一覧して、他の人のカメラを、出てきたり消えたりするのに合わせて公式のプレーヤーで再生します(出す・読む)。状態の行には転送方式が出ます。webtransport か、Safari と WebTransport のない所では websocket です(接続する)。
esbuild などでブラウザー向けにまとめ、#status と #people のあるページから読み込みます。
npx esbuild page.ts --bundle --format=esm --outfile=page.js<!doctype html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<p id="status"></p>
<ul id="people"></ul>
<script type="module" src="page.js"></script>4. 二つのブラウザーで開く
ページは https://(か localhost)で配ります。ブラウザーはカメラと WebTransport を安全なページにしか渡しません。page.html#link={link id} を二つのブラウザーで開きます。それぞれが相手のカメラと音を再生します。片方を閉じると、もう片方からその映像が消えます。
5. トークンに署名するバックエンド
リンクは全員に同じ権利と、ブラウザーから導いた ID を与えます。相手が誰かをアプリが決めるときは、代わりにバックエンドがトークンに署名します。キーの画面でキーを作り、ID とシークレットをコピーします。シークレットはこのときだけ表示されます。トークンは moq-auth の形式の JWT で、キーのシークレットで HS256 により署名し、キーの ID を kid にします(接続する)。このサンプルはパッケージ jose(npm install jose)で署名します。
// Signs tokens with a project key, from TABLEBOX_KEY_ID and TABLEBOX_KEY_SECRET (npm install jose).
import { SignJWT } from 'jose';
export interface Rights { root?: string; publish?: string[]; subscribe?: string[] }
/** A token for `sub` with `rights`, valid for ten minutes. */
export function sign(sub: string, rights: Rights): Promise<string> {
return new SignJWT({ ...rights })
.setProtectedHeader({ alg: 'HS256', typ: 'JWT', kid: process.env.TABLEBOX_KEY_ID! })
.setSubject(sub)
.setIssuedAt()
.setExpirationTime('10m')
.sign(new TextEncoder().encode(process.env.TABLEBOX_KEY_SECRET!));
}このバックエンドは、ユーザーに、その名前の下に出せて全部を読めるトークンを渡します。
// Your backend on port 8000: gives each user a token that publishes under their own name and reads everything.
import { createServer } from 'node:http';
import { sign } from './token.ts';
createServer(async (request, response) => {
// Decide here who is asking, with your own sign-in; this sample trusts the query.
const user = new URL(request.url!, 'http://localhost').searchParams.get('user') ?? '';
if (!/^[\w-]{1,64}$/u.test(user)) return void response.writeHead(400).end();
response.end(await sign(user, { publish: [`${user}/**`], subscribe: ['**'] }));
}).listen(8000);ページでは getToken で接続します。SDK は接続を試すたびに、バックエンドに新しいトークンを求めます。
// Connects as `user` with a token from your backend, which the SDK asks for before every attempt.
import { connect } from 'tablebox.io';
import { relay } from './relay.ts';
export async function connectAs(user: string) {
const getToken = () => fetch(`/token?user=${encodeURIComponent(user)}`).then(reply => reply.text());
const connection = await connect({ ...relay, getToken });
connection.status.subscribe(status => {
console.log(`${user}: ${status}`, connection.transport.peek() ?? '', connection.reason.peek()?.name ?? '');
});
return connection;
}ホストするサービスの公開まで
Tablebox のリポジトリは、Manager と二つの Relay を自分のマシンで動かします。npm run local はそれらを Console と一緒にループバックで起動し、キーとリンクのあるプロジェクトを作って、Manager と Console のアドレス、Relay の URL、その自己署名証明書の SHA-256、テスト用のトークン、リンクを表示します。この証明書は認証局が署名したものではないので、その SHA-256 を connect に serverCertificateHashes(Rust では server_certificate_hashes)として渡します。Console へのサインインは、スタックが表示するアドレスで、Manager のループバック専用の開発用サインインを使います。手順はリポジトリの ops/local/README.md にあります。
このドキュメントのサンプルはどれも、一か所で与えた値で動きます。ブラウザーのサンプルは Relay の URL と SHA-256 を relay.ts(上)から取ります。Node のサンプルはそれらを環境変数からこの relay.ts で、キーを token.ts で取ります。Rust のサンプルも同じ変数を読みます。
| 変数 | 値 |
|---|---|
TABLEBOX_RELAY_URL |
Relay の URL: ホストされたもの、またはローカルスタックが表示するもの |
TABLEBOX_RELAY_CERT_SHA256 |
ローカルスタックの証明書の SHA-256。ホストするサービスでは設定しない |
TABLEBOX_KEY_ID, TABLEBOX_KEY_SECRET |
プロジェクトのキー |
TABLEBOX_TOKEN |
トークン。Rust のサンプル用 |
TABLEBOX_MANAGER_URL, TABLEBOX_PROJECT |
Manager API のアドレスとプロジェクトの ID。管理のサンプル用 |
// The Relay URL, and for a local Relay its certificate's SHA-256, from the environment.
const hash = process.env.TABLEBOX_RELAY_CERT_SHA256;
export const relay = { url: process.env.TABLEBOX_RELAY_URL!, serverCertificateHashes: hash ? [hash] : [] };Node 22.18 以降は TypeScript のサンプルをそのまま実行します(node publish.ts)。Rust のサンプルは、クレート tablebox、serde_json、tokio を使う、tokio ランタイム上の main.rs です。