ブラウザでビデオ通話を作ろうとすると、カメラ映像を取得するだけでは終わりません。シグナリング、ルーム管理、メディア配信、NAT越え、切断からの復帰なども考える必要があります。
この記事では、Tencent RTC(以下、TRTC)のWeb SDK v5を使い、2つのブラウザ間で映像と音声を送受信できる最小構成を作ります。
完成時にできることは次のとおりです。
- ユーザーIDとルームIDを指定して入室する
- カメラとマイクを配信する
- 同じルームにいる相手の映像と音声を再生する
- 相手がカメラを停止したら表示を削除する
- 退室してデバイスを解放する
サンプルはVanilla JavaScriptで実装します。ReactやVueでも、SDK部分の考え方は同じです。
2026年8月5日時点で、npmの
trtc-sdk-v5最新版5.19.0と公式Web SDK v5ドキュメントをもとに確認しています。
前提条件
- Node.js 18以上
- カメラとマイクを利用できるPC
- TRTCの
SDKAppID - テストユーザー用の
UserSig
TRTCコンソールでRTC Engineアプリケーションを作成し、SDKAppIDを取得します。動作確認用のUserSigは、コンソールのUserSig生成ツールで作成できます。
UserSigはユーザーIDと対応しています。たとえばalice用に生成したUserSigをbobのログインに使うことはできません。2人で試す場合は、aliceとbobのそれぞれについて生成してください。
コンソール生成またはクライアント生成のUserSigは、ローカルでのテスト専用です。本番環境ではSDKSecretKeyをブラウザに置かず、必ずサーバー側でUserSigを生成してください。
1. Viteプロジェクトを作成する
npm create vite@latest trtc-web-quickstart -- --template vanilla
cd trtc-web-quickstart
npm install
npm install trtc-sdk-v5
npm run dev
開発サーバーが起動したら、ブラウザでhttp://localhost:5173を開きます。
ブラウザは安全なコンテキストでのみカメラとマイクへのアクセスを許可します。ローカル開発ではhttp://localhostを利用でき、本番環境ではHTTPSが必要です。
2. HTMLを作る
index.htmlを次の内容に置き換えます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TRTC Web Quickstart</title>
</head>
<body>
<main class="app">
<h1>TRTC Web Quickstart</h1>
<section class="settings">
<label>
SDKAppID
<input id="sdkAppId" inputmode="numeric" placeholder="1400000000" />
</label>
<label>
User ID
<input id="userId" placeholder="alice" />
</label>
<label>
UserSig
<textarea id="userSig" rows="4" placeholder="eJw...カット"></textarea>
</label>
<label>
Room ID
<input id="roomId" inputmode="numeric" value="8888" />
</label>
</section>
<section class="actions">
<button id="join">入室</button>
<button id="leave" disabled>退室</button>
</section>
<p id="status">未接続</p>
<section class="videos">
<article class="video-card">
<h2>自分</h2>
<div id="local-video" class="video-view"></div>
</article>
<div id="remote-video-list" class="remote-list"></div>
</section>
</main>
<script type="module" src="/src/main.js"></script>
</body>
</html>
3. SDKを初期化し、ルームに入る
src/main.jsを次の内容に置き換えます。
import './style.css';
import TRTC from 'trtc-sdk-v5';
const trtc = TRTC.create();
let joined = false;
const sdkAppIdInput = document.querySelector('#sdkAppId');
const userIdInput = document.querySelector('#userId');
const userSigInput = document.querySelector('#userSig');
const roomIdInput = document.querySelector('#roomId');
const joinButton = document.querySelector('#join');
const leaveButton = document.querySelector('#leave');
const statusText = document.querySelector('#status');
const remoteVideoList = document.querySelector('#remote-video-list');
function setStatus(message) {
statusText.textContent = message;
}
function setJoined(nextJoined) {
joined = nextJoined;
joinButton.disabled = nextJoined;
leaveButton.disabled = !nextJoined;
}
function getRoomParameters() {
const sdkAppId = Number(sdkAppIdInput.value);
const roomId = Number(roomIdInput.value);
const userId = userIdInput.value.trim();
const userSig = userSigInput.value.trim();
if (!Number.isInteger(sdkAppId) || sdkAppId <= 0) {
throw new Error('SDKAppIDを正しく入力してください');
}
if (!Number.isInteger(roomId) || roomId <= 0) {
throw new Error('Room IDを正しく入力してください');
}
if (!userId || !userSig) {
throw new Error('User IDとUserSigを入力してください');
}
return { sdkAppId, roomId, userId, userSig };
}
function remoteViewId(userId, streamType) {
return `remote-${userId}-${streamType}`;
}
function addRemoteView(userId, streamType) {
const id = remoteViewId(userId, streamType);
let view = document.getElementById(id);
if (view) return id;
const card = document.createElement('article');
card.className = 'video-card';
card.dataset.viewId = id;
const title = document.createElement('h2');
title.textContent = `${userId} / ${streamType}`;
view = document.createElement('div');
view.id = id;
view.className = 'video-view';
card.append(title, view);
remoteVideoList.append(card);
return id;
}
function removeRemoteView(userId, streamType) {
const id = remoteViewId(userId, streamType);
const card = remoteVideoList.querySelector(`[data-view-id="${id}"]`);
card?.remove();
}
// イベントはenterRoom()より前に登録する
trtc.on(TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, async ({ userId, streamType }) => {
const view = addRemoteView(userId, streamType);
try {
await trtc.startRemoteVideo({ userId, streamType, view });
} catch (error) {
console.error('遠隔映像の再生に失敗しました', error);
removeRemoteView(userId, streamType);
}
});
trtc.on(TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, ({ userId, streamType }) => {
// SDK側では自動的に再生が止まるため、ここではUIだけを削除する
removeRemoteView(userId, streamType);
});
trtc.on(TRTC.EVENT.KICKED_OUT, ({ reason, message }) => {
console.error('ルームから退出させられました', { reason, message });
remoteVideoList.replaceChildren();
setJoined(false);
setStatus(`切断されました: ${reason}`);
});
joinButton.addEventListener('click', async () => {
if (joined) return;
try {
joinButton.disabled = true;
setStatus('環境を確認しています...');
const support = await TRTC.isSupported();
if (!support.result) {
throw new Error('このブラウザまたは接続方式ではTRTCを利用できません');
}
const { sdkAppId, roomId, userId, userSig } = getRoomParameters();
setStatus('入室しています...');
await trtc.enterRoom({ sdkAppId, roomId, userId, userSig });
// カメラとマイクを取得し、ルームへ公開する
await trtc.startLocalVideo({ view: 'local-video' });
await trtc.startLocalAudio();
setJoined(true);
setStatus(`Room ${roomId} に ${userId} として入室しました`);
} catch (error) {
console.error(error);
// enterRoom後のデバイス取得で失敗した場合も、可能なら状態を戻す
await Promise.allSettled([
trtc.stopLocalVideo(),
trtc.stopLocalAudio(),
trtc.exitRoom(),
]);
setJoined(false);
setStatus(error instanceof Error ? error.message : String(error));
}
});
leaveButton.addEventListener('click', async () => {
if (!joined) return;
try {
leaveButton.disabled = true;
setStatus('退室しています...');
await Promise.allSettled([
trtc.stopLocalVideo(),
trtc.stopLocalAudio(),
]);
await trtc.exitRoom();
remoteVideoList.replaceChildren();
setJoined(false);
setStatus('退室しました');
} catch (error) {
console.error(error);
setStatus(error instanceof Error ? error.message : String(error));
}
});
処理の中心は次の5つです。
-
TRTC.create()でSDKインスタンスを作る -
trtc.enterRoom()でルームに入る -
startLocalVideo()とstartLocalAudio()で自分の映像・音声を公開する -
REMOTE_VIDEO_AVAILABLEを受け、startRemoteVideo()で相手の映像を再生する -
exitRoom()で退室する
遠隔音声はデフォルトで自動再生されるため、このサンプルでは音声再生用APIを明示的に呼んでいません。
4. 最低限のCSSを追加する
src/style.cssを次の内容に置き換えます。
:root {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #f4f7fb;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
button,
input,
textarea {
font: inherit;
}
.app {
width: min(1040px, calc(100% - 32px));
margin: 40px auto;
}
.settings {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
padding: 20px;
background: #ffffff;
border: 1px solid #dce3ef;
border-radius: 16px;
}
label {
display: grid;
gap: 8px;
font-weight: 700;
}
label:has(textarea) {
grid-column: 1 / -1;
}
input,
textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #b9c4d6;
border-radius: 8px;
}
.actions {
display: flex;
gap: 12px;
margin: 20px 0 8px;
}
button {
padding: 10px 18px;
border: 0;
border-radius: 8px;
color: #ffffff;
background: #315efb;
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.videos,
.remote-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.remote-list {
display: contents;
}
.video-card {
overflow: hidden;
background: #ffffff;
border: 1px solid #dce3ef;
border-radius: 16px;
}
.video-card h2 {
margin: 0;
padding: 12px 16px;
font-size: 14px;
}
.video-view {
width: 100%;
aspect-ratio: 16 / 9;
background: #101522;
}
5. 2人で通話を確認する
- ブラウザでページを2つ開く
- 片方は
alice、もう片方はbobを入力する - それぞれのユーザーIDに対応するUserSigを入力する
- 両方で同じRoom IDを入力する
- 両方の「入室」を押す
同じルームに入ると、相手の映像が追加され、音声が再生されます。同じユーザーIDで重複入室すると、先に入室した側がKICKED_OUTになるため、ユーザーIDは必ず分けます。
実装時につまずきやすい点
カメラとマイクを取得できない
本番環境がHTTPになっていないか確認してください。カメラ・マイクを配信するページはHTTPSで公開する必要があります。ローカルではhttp://localhostを使います。
OSまたはブラウザ側でカメラ・マイクを拒否していないかも確認します。
映像は出るが音が出ない
ブラウザの自動再生ポリシーが原因の可能性があります。今回の例はユーザーが「入室」をクリックしてからルームへ入るため制限を受けにくい構成ですが、ページ表示直後に自動入室させる設計では、音声再生を開始するユーザー操作を別途用意してください。
相手の映像が表示されない
REMOTE_VIDEO_AVAILABLEのリスナーをenterRoom()より前に登録しているか確認します。また、startRemoteVideo()へ渡すviewと、実際のDOM要素のIDが一致している必要があります。
UserSigエラーになる
次の組み合わせが一致しているか確認します。
- UserSigを発行したTRTCアプリケーションの
SDKAppID - UserSig生成時に指定した
UserID -
enterRoom()に渡したuserId - UserSigの有効期限
本番化する前に追加したいもの
このサンプルは通話の最小構成です。本番では少なくとも次を追加します。
- サーバー側のUserSig発行API
- 入室前のカメラ、マイク、スピーカー、ネットワーク検査
- カメラ・マイクのON/OFF操作
- デバイス切り替え
- 自動再生が拒否された場合のリカバリーUI
- 切断・再接続状態の表示
- ルームへの参加権限と業務側認証
- 通話品質とエラーログの監視
- 利用目的に合ったプライバシー表示と同意取得
TRTC Web SDK v5にはデバイス検査用プラグインも用意されているため、プロトタイプの次は入室前チェックを組み込むと、実利用時のトラブルを減らせます。
まとめ
TRTC Web SDK v5では、ルームへの入室、ローカルメディアの公開、遠隔映像の再生をAPI単位で実装できます。
最小構成の要点は次のとおりです。
TRTC.create()
-> enterRoom()
-> startLocalVideo() / startLocalAudio()
-> REMOTE_VIDEO_AVAILABLE
-> startRemoteVideo()
-> exitRoom()
まずは2つのブラウザで動かし、その後にUserSigのサーバー発行、デバイス検査、再接続時のUIを足していくと、本番構成へ段階的に進められます。