webrtc について
WebRTCのピア接続、シグナリング、データチャネル、画面共有の実装例をまとめたスキルです。グループ通話向けのLiveKitと、接続の信頼性を確認する本番チェック項目も扱います。
主な機能
- 01ピア接続の設定
- 02Socket.IOシグナリング
- 03データチャネルでのメッセージ交換
- 04画面共有
- 05LiveKitのグループ通話例
- 06本番環境の信頼性チェック
活用シーン
- 011対1のWebRTC通話を構築する
- 02データチャネルでメッセージを交換する
- 03通話中に画面を共有する
- 04SFUを使ったグループ通話を計画する
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill webrtcインストール後、Claude Code で/webrtcを入力して読み込まれたか確認します。
詳細
WebRTC
ピア接続のセットアップ
const config: RTCConfiguration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:turn.example.com', username: 'user', credential: 'pass' },
],
};
const pc = new RTCPeerConnection(config);
// Get local media
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
// Display remote stream
pc.ontrack = (event) => {
remoteVideo.srcObject = event.streams[0];
};
// ICE candidates — send to remote peer via signaling
pc.onicecandidate = (event) => {
if (event.candidate) {
signalingChannel.send({ type: 'ice-candidate', candidate: event.candidate });
}
};
シグナリング(Socket.IO 経由)
// Caller
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit('signal', { type: 'offer', sdp: offer });
// Callee — on receiving offer
socket.on('signal', async (msg) => {
if (msg.type === 'offer') {
await pc.setRemoteDescription(msg.sdp);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('signal', { type: 'answer', sdp: answer });
} else if (msg.type === 'answer') {
await pc.setRemoteDescription(msg.sdp);
} else if (msg.type === 'ice-candidate') {
await pc.addIceCandidate(msg.candidate);
}
});
データチャネル
const dataChannel = pc.createDataChannel('chat', { ordered: true });
dataChannel.onopen = () => dataChannel.send('Hello!');
dataChannel.onmessage = (e) => console.log('Received:', e.data);
// Remote side
pc.ondatachannel = (event) => {
const channel = event.channel;
channel.onmessage = (e) => console.log('Received:', e.data);
};
画面共有
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: { cursor: 'always' },
audio: true,
});
const videoTrack = screenStream.getVideoTracks()[0];
const sender = pc.getSenders().find((s) => s.track?.kind === 'video');
await sender?.replaceTrack(videoTrack);
videoTrack.onended = () => {
// User stopped sharing — switch back to camera
const cameraTrack = localStream.getVideoTracks()[0];
sender?.replaceTrack(cameraTrack);
};
LiveKit(SFU — グループ通話に推奨)
import { Room, RoomEvent } from 'livekit-client';
const room = new Room();
await room.connect(LIVEKIT_URL, accessToken);
await room.localParticipant.enableCameraAndMicrophone();
room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
const element = track.attach();
document.getElementById('remote-videos')!.appendChild(element);
});
room.on(RoomEvent.ParticipantDisconnected, (participant) => {
console.log(`${participant.identity} left`);
});
アンチパターン
| アンチパターン | 対処方法 |
|---|---|
| TURN サーバーがない | NAT 越えのため、常に TURN を設定します |
| グループ通話(4 人超)で P2P を使用する | SFU(LiveKit、mediasoup)を使用します |
| getUserMedia のエラーハンドリングがない | NotAllowedError、NotFoundError を処理します |
| 暗号化されていないチャネルでシグナリングする | WSS(WebSocket Secure)を使用します |
| 接続状態を監視しない | connectionstatechange イベントをリッスンします |
本番環境のチェックリスト
- TURN サーバーをデプロイして設定する
- グループビデオ(参加者 3 人超)には SFU を使用する
- メディアへのアクセスが拒否された場合のフォールバック UI
- 接続品質の監視
- 帯域幅の適応(シマルキャスト)
- 必要に応じて録画機能を用意する
コメント
Markdown 対応