LiveKit:リアルタイム音声・映像通信フレームワークガイド
LiveKitは、WebRTCベースのオープンソースなリアルタイム音声・映像通信フレームワークです。この記事では、LiveKitの基本から実践的な活用法まで、コード例とともに詳しく解説します。
LiveKitとは
LiveKitは、ビデオ会議、ライブ配信、リアルタイムコラボレーションなどのアプリケーションを構築するためのインフラストラクチャです。
主な特徴
- スケーラブル - 数千人規模のセッションに対応
- 低レイテンシ - リアルタイム通信に最適化
- オープンソース - Apache 2.0ライセンス
- マルチプラットフォーム - Web、iOS、Android、Flutter、Unity対応
- エンタープライズ対応 - E2E暗号化、録画、分析機能
- 自己ホスト可能 - セルフホスト版とクラウド版を選択可能
セットアップ
サーバーのセットアップ(Docker)
# LiveKitサーバーをDockerで起動
docker run -d \
--name livekit \
-p 7880:7880 \
-p 7881:7881 \
-p 7882:7882/udp \
-v $PWD/livekit.yaml:/livekit.yaml \
livekit/livekit-server \
--config /livekit.yaml
livekit.yaml設定ファイル
port: 7880
rtc:
port_range_start: 50000
port_range_end: 60000
use_external_ip: true
keys:
APIxxxxxxxxxxx: SECRETxxxxxxxxxxxxxxxxxxxxxxxxx
room:
auto_create: true
empty_timeout: 300
max_participants: 100
logging:
level: info
クライアント側のインストール
# React/Next.jsプロジェクト
npm install livekit-client @livekit/components-react
# TypeScript型定義も含まれています
基本的な使い方
アクセストークンの生成
LiveKitに接続するには、サーバー側でアクセストークンを生成する必要があります。
// app/api/token/route.ts (Next.js)
import { AccessToken } from 'livekit-server-sdk';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
const roomName = req.nextUrl.searchParams.get('room');
const participantName = req.nextUrl.searchParams.get('username');
if (!roomName || !participantName) {
return NextResponse.json(
{ error: 'Missing room or username' },
{ status: 400 }
);
}
const apiKey = process.env.LIVEKIT_API_KEY;
const apiSecret = process.env.LIVEKIT_API_SECRET;
const at = new AccessToken(apiKey, apiSecret, {
identity: participantName,
});
// ルームへの参加権限を付与
at.addGrant({
roomJoin: true,
room: roomName,
canPublish: true,
canSubscribe: true,
});
const token = await at.toJwt();
return NextResponse.json({ token });
}
環境変数の設定
# .env.local
LIVEKIT_API_KEY=APIxxxxxxxxxxx
LIVEKIT_API_SECRET=SECRETxxxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880
ビデオ会議アプリの実装
LiveKitルームコンポーネント
// components/VideoCall.tsx
'use client';
import { LiveKitRoom, VideoConference } from '@livekit/components-react';
import '@livekit/components-styles';
import { useEffect, useState } from 'react';
interface VideoCallProps {
roomName: string;
userName: string;
}
export default function VideoCall({ roomName, userName }: VideoCallProps) {
const [token, setToken] = useState<string>('');
useEffect(() => {
const getToken = async () => {
const response = await fetch(
`/api/token?room=${roomName}&username=${userName}`
);
const data = await response.json();
setToken(data.token);
};
getToken();
}, [roomName, userName]);
if (!token) {
return <div>Loading...</div>;
}
return (
<LiveKitRoom
video={true}
audio={true}
token={token}
serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL}
data-lk-theme="default"
style={{ height: '100vh' }}
>
{/* VideoConferenceコンポーネントで基本的なUIを提供 */}
<VideoConference />
</LiveKitRoom>
);
}
カスタムUIの構築
// components/CustomVideoRoom.tsx
'use client';
import {
LiveKitRoom,
RoomAudioRenderer,
useParticipants,
useTracks,
ParticipantTile,
ControlBar,
} from '@livekit/components-react';
import { Track } from 'livekit-client';
import '@livekit/components-styles';
function ParticipantList() {
const participants = useParticipants();
const tracks = useTracks([
{ source: Track.Source.Camera, withPlaceholder: true },
{ source: Track.Source.ScreenShare, withPlaceholder: false },
]);
return (
<div className="grid grid-cols-2 gap-4 p-4">
{tracks.map((track) => (
<ParticipantTile
key={track.participant.identity}
participant={track.participant}
source={track.source}
/>
))}
</div>
);
}
export default function CustomVideoRoom({ token, serverUrl }: Props) {
return (
<LiveKitRoom
video={true}
audio={true}
token={token}
serverUrl={serverUrl}
className="h-screen flex flex-col"
>
{/* 音声のレンダリング */}
<RoomAudioRenderer />
{/* ビデオグリッド */}
<div className="flex-1 overflow-auto">
<ParticipantList />
</div>
{/* コントロールバー(ミュート、カメラON/OFF等) */}
<ControlBar />
</LiveKitRoom>
);
}
画面共有の実装
// hooks/useScreenShare.ts
import { useLocalParticipant } from '@livekit/components-react';
import { Track } from 'livekit-client';
import { useState } from 'react';
export function useScreenShare() {
const { localParticipant } = useLocalParticipant();
const [isSharing, setIsSharing] = useState(false);
const toggleScreenShare = async () => {
if (!localParticipant) return;
try {
if (isSharing) {
// 画面共有を停止
await localParticipant.setScreenShareEnabled(false);
setIsSharing(false);
} else {
// 画面共有を開始
await localParticipant.setScreenShareEnabled(true);
setIsSharing(true);
}
} catch (error) {
console.error('Screen share error:', error);
}
};
return { isSharing, toggleScreenShare };
}
画面共有ボタンコンポーネント
// components/ScreenShareButton.tsx
'use client';
import { useScreenShare } from '@/hooks/useScreenShare';
export default function ScreenShareButton() {
const { isSharing, toggleScreenShare } = useScreenShare();
return (
<button
onClick={toggleScreenShare}
className={`px-4 py-2 rounded ${
isSharing ? 'bg-red-500' : 'bg-blue-500'
} text-white`}
>
{isSharing ? '画面共有を停止' : '画面を共有'}
</button>
);
}
録画機能
LiveKitは録画機能(Egress)を提供しています。
サーバー側で録画開始
// app/api/recording/start/route.ts
import { EgressClient, RoomCompositeEgressRequest } from 'livekit-server-sdk';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const { roomName } = await req.json();
const egressClient = new EgressClient(
process.env.LIVEKIT_URL!,
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!
);
const output = {
filepath: `/recordings/${roomName}-${Date.now()}.mp4`,
};
const request: RoomCompositeEgressRequest = {
roomName,
layout: 'grid',
audioOnly: false,
videoOnly: false,
file: output,
};
try {
const egress = await egressClient.startRoomCompositeEgress(request);
return NextResponse.json({ egressId: egress.egressId });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to start recording' },
{ status: 500 }
);
}
}
録画停止
// app/api/recording/stop/route.ts
import { EgressClient } from 'livekit-server-sdk';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const { egressId } = await req.json();
const egressClient = new EgressClient(
process.env.LIVEKIT_URL!,
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!
);
try {
await egressClient.stopEgress(egressId);
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to stop recording' },
{ status: 500 }
);
}
}
ルーム管理
ルーム一覧の取得
// app/api/rooms/route.ts
import { RoomServiceClient } from 'livekit-server-sdk';
import { NextResponse } from 'next/server';
export async function GET() {
const roomService = new RoomServiceClient(
process.env.LIVEKIT_URL!,
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!
);
try {
const rooms = await roomService.listRooms();
return NextResponse.json({ rooms });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch rooms' },
{ status: 500 }
);
}
}
参加者の管理
// 参加者の削除
const removeParticipant = async (roomName: string, identity: string) => {
const roomService = new RoomServiceClient(
process.env.LIVEKIT_URL!,
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!
);
await roomService.removeParticipant(roomName, identity);
};
// 参加者のミュート
const muteParticipant = async (roomName: string, identity: string) => {
const roomService = new RoomServiceClient(
process.env.LIVEKIT_URL!,
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!
);
await roomService.mutePublishedTrack(roomName, identity, 'audio');
};
データメッセージの送信
// components/ChatInRoom.tsx
'use client';
import { useLocalParticipant, useDataChannel } from '@livekit/components-react';
import { useState } from 'react';
export default function ChatInRoom() {
const { localParticipant } = useLocalParticipant();
const [messages, setMessages] = useState<string[]>([]);
const [input, setInput] = useState('');
// データチャネルでメッセージを受信
useDataChannel((message) => {
const decoder = new TextDecoder();
const text = decoder.decode(message.payload);
setMessages((prev) => [...prev, `${message.from?.identity}: ${text}`]);
});
const sendMessage = () => {
if (!localParticipant || !input) return;
const encoder = new TextEncoder();
const data = encoder.encode(input);
localParticipant.publishData(data, { reliable: true });
setInput('');
};
return (
<div className="p-4">
<div className="h-64 overflow-y-auto border p-2 mb-2">
{messages.map((msg, i) => (
<div key={i}>{msg}</div>
))}
</div>
<div className="flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
className="flex-1 border px-2 py-1"
placeholder="メッセージを入力"
/>
<button onClick={sendMessage} className="px-4 py-1 bg-blue-500 text-white">
送信
</button>
</div>
</div>
);
}
WebHookの活用
LiveKitはルームイベントをWebHookで通知できます。
// app/api/webhook/livekit/route.ts
import { WebhookReceiver } from 'livekit-server-sdk';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const receiver = new WebhookReceiver(
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!
);
const body = await req.text();
const authHeader = req.headers.get('Authorization');
try {
const event = receiver.receive(body, authHeader!);
// イベントタイプに応じて処理
switch (event.event) {
case 'room_started':
console.log('Room started:', event.room);
break;
case 'room_finished':
console.log('Room finished:', event.room);
break;
case 'participant_joined':
console.log('Participant joined:', event.participant);
break;
case 'participant_left':
console.log('Participant left:', event.participant);
break;
}
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({ error: 'Invalid webhook' }, { status: 400 });
}
}
デプロイ時の注意点
HTTPS/WSS必須
本番環境ではHTTPS/WSSが必須です。
# Vercel等にデプロイする場合
NEXT_PUBLIC_LIVEKIT_URL=wss://your-livekit-server.com
ファイアウォール設定
LiveKitは以下のポートを使用します。
- 7880 - HTTP/WebSocket
- 7881 - TURN/STUN over TCP
- 50000-60000 - WebRTC メディア(UDP)
プロダクション向け設定
# livekit.yaml (本番環境)
port: 7880
rtc:
port_range_start: 50000
port_range_end: 60000
use_external_ip: true
ice_servers:
- urls:
- stun:stun.l.google.com:19302
turn:
enabled: true
domain: turn.yourdomain.com
tls_port: 5349
udp_port: 3478
logging:
level: warn
# 録画用ストレージ
egress:
s3:
access_key: YOUR_ACCESS_KEY
secret: YOUR_SECRET
region: us-east-1
bucket: your-bucket
まとめ
LiveKitの主な機能をまとめます。
- 簡単なセットアップ - Dockerで即座に起動可能
- リアルタイム通信 - WebRTCベースの低レイテンシ通信
- 画面共有 - ワンクリックで画面共有が可能
- 録画機能 - Egressでルーム全体を録画
- ルーム管理 - APIで参加者やルームを管理
- データチャネル - チャット等のテキスト通信
- WebHook - イベント駆動の処理
LiveKitを使えば、ZoomやGoogle Meetのようなビデオ会議アプリを短時間で構築できます。セルフホスト版なら完全に自社管理できるため、エンタープライズ用途にも最適です。
リアルタイム通信が必要なアプリケーションを開発するなら、LiveKitは最有力の選択肢です。