Tablebox ドキュメントサイト English

使い始める

Console でプロジェクト、リンク、キーを作り、リンクで接続してカメラとマイクを出し、他の全員のものを再生するブラウザーのページを書いて、二つのブラウザーで開きます。それから、アプリが相手が誰で何をしてよいかを決めるときのために、トークンに署名するバックエンドを足します。

ホストするサービスはまだ公開していません。 その Console と Relay はまだないので、この手順はまだそこでは進められません。自分のマシンで進める方法はホストするサービスの公開までにあります。

1. サインインしてプロジェクトを作る

Console のサインイン画面を開き、GitHub でサインインします。プロジェクトの画面で、名前を付けてプロジェクトを作ります。そのプロジェクトの画面に、プロジェクトの ID と、アプリが接続する Relay の URL が出ます。

リンクは、その 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 を書きます。

TypeScript(ブラウザー) · relay.ts
// 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[] };
TypeScript(ブラウザー) · start.ts
// 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)で署名します。

TypeScript(Node) · token.ts
// 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!));
}

このバックエンドは、ユーザーに、その名前の下に出せて全部を読めるトークンを渡します。

TypeScript(Node) · backend.ts
// 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 は接続を試すたびに、バックエンドに新しいトークンを求めます。

TypeScript(ブラウザー) · connect.ts
// 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。管理のサンプル用
TypeScript(Node) · relay.ts
// 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 です。