بناء تطبيق فيديو ومكالمات صوتية في الوقت الحقيقي مع LiveKit و Next.js

التواصل في الوقت الحقيقي عبر الفيديو والصوت أصبح من المتطلبات الأساسية في التطبيقات الحديثة — من مؤتمرات الفيديو إلى وكلاء الذكاء الاصطناعي الصوتيين إلى البث المباشر. لكن بناء هذه الأنظمة من الصفر باستخدام WebRTC مباشرة أمر معقد للغاية: تحتاج لإدارة خوادم TURN/STUN، والتفاوض على الجلسات، والتعامل مع مشاكل الشبكة.
LiveKit يحل هذه المشكلة بتوفير بنية تحتية مفتوحة المصدر للتواصل في الوقت الحقيقي. يدير لك كل تعقيدات WebRTC ويقدم واجهات برمجة تطبيقات بسيطة لإنشاء الغرف وإدارة المشاركين وبث الوسائط. مع مكتبات React الجاهزة، يمكنك بناء تطبيق مكالمات فيديو كامل بأقل جهد.
في هذا الدليل، ستبني تطبيق مؤتمرات فيديو يدعم:
- غرف متعددة المشاركين مع فيديو وصوت
- مشاركة الشاشة
- التحكم بالميكروفون والكاميرا
- عرض شبكي للمشاركين
- إنشاء رموز وصول آمنة من الخادم
- واجهة مستخدم متجاوبة وعصرية
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت
- معرفة أساسية بـ React و TypeScript
- إلمام بـ Next.js App Router
- حساب مجاني على LiveKit Cloud (أو خادم LiveKit محلي عبر Docker)
- محرر أكواد (يُنصح بـ VS Code)
ما ستبنيه
تطبيق مؤتمرات فيديو كامل يتضمن:
- صفحة انضمام — يُدخل المستخدم اسمه واسم الغرفة ثم ينضم
- غرفة فيديو — عرض شبكي لجميع المشاركين مع بثهم المباشر
- شريط أدوات — أزرار للتحكم بالميكروفون والكاميرا ومشاركة الشاشة والمغادرة
- واجهة API آمنة — إنشاء رموز وصول JWT من جانب الخادم
- حالة الاتصال — مؤشرات بصرية لحالة كل مشارك
الخطوة 1: إنشاء مشروع Next.js
أنشئ مشروع Next.js 15 جديد:
npx create-next-app@latest livekit-video --typescript --tailwind --eslint --app --src-dir --use-npm
cd livekit-videoثبّت حزم LiveKit:
npm install livekit-client livekit-server-sdk @livekit/components-react @livekit/components-styles- livekit-client — مكتبة العميل للتواصل مع خادم LiveKit
- livekit-server-sdk — إنشاء رموز الوصول من جانب الخادم
- @livekit/components-react — مكونات React جاهزة للفيديو والصوت
- @livekit/components-styles — أنماط CSS الافتراضية للمكونات
الخطوة 2: إعداد متغيرات البيئة
أنشئ ملف .env.local في جذر المشروع:
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secretالحصول على المفاتيح
- سجل في LiveKit Cloud
- أنشئ مشروعًا جديدًا
- انسخ URL و API Key و API Secret من لوحة التحكم
بدلاً من ذلك، يمكنك تشغيل خادم LiveKit محليًا باستخدام Docker:
docker run --rm -p 7880:7880 -p 7881:7881 -p 7882:7882/udp \
-e LIVEKIT_KEYS="devkey: secret" \
livekit/livekit-serverفي هذه الحالة، استخدم:
LIVEKIT_URL=ws://localhost:7880
LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=secretالخطوة 3: إنشاء واجهة API لرموز الوصول
أنشئ ملف src/app/api/token/route.ts:
import { AccessToken } from "livekit-server-sdk";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const { roomName, participantName } = await request.json();
if (!roomName || !participantName) {
return NextResponse.json(
{ error: "roomName and participantName are required" },
{ status: 400 }
);
}
const apiKey = process.env.LIVEKIT_API_KEY;
const apiSecret = process.env.LIVEKIT_API_SECRET;
if (!apiKey || !apiSecret) {
return NextResponse.json(
{ error: "Server misconfigured" },
{ status: 500 }
);
}
const token = new AccessToken(apiKey, apiSecret, {
identity: participantName,
name: participantName,
});
token.addGrant({
room: roomName,
roomJoin: true,
canPublish: true,
canSubscribe: true,
canPublishData: true,
});
const jwt = await token.toJwt();
return NextResponse.json({ token: jwt });
}هذه الواجهة تقوم بـ:
- استقبال اسم الغرفة واسم المشارك من طلب POST
- التحقق من وجود البيانات المطلوبة
- إنشاء رمز وصول JWT باستخدام LiveKit Server SDK
- منح الصلاحيات — الانضمام للغرفة، النشر، والاشتراك
- إرجاع الرمز للعميل
الرمز يمنح المشارك كامل الصلاحيات: البث، الاستقبال، وإرسال البيانات. في بيئة الإنتاج، خصص الصلاحيات حسب دور المستخدم.
الخطوة 4: بناء صفحة الانضمام
أنشئ ملف src/app/page.tsx:
"use client";
import { useState, FormEvent } from "react";
import { useRouter } from "next/navigation";
export default function JoinPage() {
const [participantName, setParticipantName] = useState("");
const [roomName, setRoomName] = useState("");
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
async function handleJoin(e: FormEvent) {
e.preventDefault();
if (!participantName.trim() || !roomName.trim()) return;
setIsLoading(true);
const params = new URLSearchParams({
room: roomName.trim(),
name: participantName.trim(),
});
router.push(`/room?${params.toString()}`);
}
return (
<main className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="w-full max-w-md p-8 bg-gray-900 rounded-2xl shadow-2xl">
<h1 className="text-3xl font-bold text-white text-center mb-2">
LiveKit Video
</h1>
<p className="text-gray-400 text-center mb-8">
انضم إلى غرفة مكالمة فيديو
</p>
<form onSubmit={handleJoin} className="space-y-5">
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-300 mb-1"
>
اسمك
</label>
<input
id="name"
type="text"
value={participantName}
onChange={(e) => setParticipantName(e.target.value)}
placeholder="أدخل اسمك"
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
<label
htmlFor="room"
className="block text-sm font-medium text-gray-300 mb-1"
>
اسم الغرفة
</label>
<input
id="room"
type="text"
value={roomName}
onChange={(e) => setRoomName(e.target.value)}
placeholder="أدخل اسم الغرفة"
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 text-white font-semibold rounded-lg transition-colors"
>
{isLoading ? "جارٍ الانضمام..." : "انضم للغرفة"}
</button>
</form>
</div>
</main>
);
}صفحة بسيطة تحتوي نموذجًا لإدخال اسم المشارك واسم الغرفة. عند الإرسال، يُوجَّه المستخدم لصفحة الغرفة مع المعلومات في معاملات URL.
الخطوة 5: بناء مكون غرفة الفيديو
أنشئ ملف src/components/VideoRoom.tsx:
"use client";
import { useEffect, useState } from "react";
import {
LiveKitRoom,
VideoConference,
RoomAudioRenderer,
ControlBar,
GridLayout,
ParticipantTile,
useTracks,
} from "@livekit/components-react";
import "@livekit/components-styles";
import { Track } from "livekit-client";
interface VideoRoomProps {
roomName: string;
participantName: string;
onLeave: () => void;
}
export function VideoRoom({
roomName,
participantName,
onLeave,
}: VideoRoomProps) {
const [token, setToken] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchToken() {
try {
const response = await fetch("/api/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roomName, participantName }),
});
if (!response.ok) {
throw new Error("Failed to get access token");
}
const data = await response.json();
setToken(data.token);
} catch (err) {
setError(err instanceof Error ? err.message : "Connection error");
}
}
fetchToken();
}, [roomName, participantName]);
if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="text-center">
<p className="text-red-400 text-lg mb-4">{error}</p>
<button
onClick={onLeave}
className="px-6 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600"
>
العودة
</button>
</div>
</div>
);
}
if (!token) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="text-white text-lg">جارٍ الاتصال بالغرفة...</div>
</div>
);
}
return (
<LiveKitRoom
token={token}
serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL}
connect={true}
onDisconnected={onLeave}
data-lk-theme="default"
style={{ height: "100vh" }}
>
<VideoConference />
<RoomAudioRenderer />
</LiveKitRoom>
);
}هذا المكون يقوم بـ:
- جلب رمز الوصول من واجهة API عند التحميل
- عرض حالة التحميل أثناء جلب الرمز
- عرض الخطأ مع زر العودة إذا فشل الاتصال
- الاتصال بالغرفة عبر
LiveKitRoomبعد الحصول على الرمز - عرض واجهة المؤتمر باستخدام
VideoConferenceالجاهز
مكون VideoConference من LiveKit يوفر واجهة كاملة تشمل عرض المشاركين وشريط الأدوات ومشاركة الشاشة تلقائيًا.
الخطوة 6: بناء صفحة الغرفة
أنشئ ملف src/app/room/page.tsx:
"use client";
import { useSearchParams, useRouter } from "next/navigation";
import { Suspense } from "react";
import { VideoRoom } from "@/components/VideoRoom";
function RoomContent() {
const searchParams = useSearchParams();
const router = useRouter();
const roomName = searchParams.get("room");
const participantName = searchParams.get("name");
if (!roomName || !participantName) {
router.push("/");
return null;
}
return (
<VideoRoom
roomName={roomName}
participantName={participantName}
onLeave={() => router.push("/")}
/>
);
}
export default function RoomPage() {
return (
<Suspense
fallback={
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="text-white text-lg">جارٍ التحميل...</div>
</div>
}
>
<RoomContent />
</Suspense>
);
}صفحة الغرفة تستخرج المعلومات من URL وتمررها لمكون VideoRoom. إذا كانت المعلومات ناقصة، يُعاد توجيه المستخدم لصفحة الانضمام.
الخطوة 7: إضافة متغير البيئة العام
أضف NEXT_PUBLIC_LIVEKIT_URL لملف .env.local:
NEXT_PUBLIC_LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secretالمتغير بادئة NEXT_PUBLIC_ يجعله متاحًا في كود العميل — مطلوب لتوصيل LiveKitRoom بالخادم.
الخطوة 8: بناء مكون فيديو مخصص
المكون VideoConference الجاهز ممتاز للبداية السريعة. لكن لتخصيص الواجهة بالكامل، ابنِ مكونك الخاص. أنشئ ملف src/components/CustomVideoRoom.tsx:
"use client";
import { useEffect, useState, useCallback } from "react";
import {
LiveKitRoom,
RoomAudioRenderer,
GridLayout,
ParticipantTile,
useTracks,
useParticipants,
useLocalParticipant,
TrackToggle,
DisconnectButton,
} from "@livekit/components-react";
import "@livekit/components-styles";
import { Track, RoomEvent } from "livekit-client";
interface CustomVideoRoomProps {
roomName: string;
participantName: string;
onLeave: () => void;
}
function StageArea() {
const tracks = useTracks(
[
{ source: Track.Source.Camera, withPlaceholder: true },
{ source: Track.Source.ScreenShare, withPlaceholder: false },
],
{ onlySubscribed: false }
);
return (
<GridLayout
tracks={tracks}
style={{ height: "calc(100vh - 80px)" }}
>
<ParticipantTile />
</GridLayout>
);
}
function CustomControlBar() {
const participants = useParticipants();
return (
<div className="h-20 bg-gray-900 border-t border-gray-800 flex items-center justify-between px-6">
<div className="text-gray-400 text-sm">
{participants.length} مشارك{participants.length > 1 ? "ين" : ""}
</div>
<div className="flex items-center gap-3">
<TrackToggle
source={Track.Source.Microphone}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-full transition-colors"
/>
<TrackToggle
source={Track.Source.Camera}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-full transition-colors"
/>
<TrackToggle
source={Track.Source.ScreenShare}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-full transition-colors"
/>
<DisconnectButton className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-full transition-colors">
مغادرة
</DisconnectButton>
</div>
<div className="w-24" />
</div>
);
}
export function CustomVideoRoom({
roomName,
participantName,
onLeave,
}: CustomVideoRoomProps) {
const [token, setToken] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchToken() {
try {
const response = await fetch("/api/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roomName, participantName }),
});
if (!response.ok) throw new Error("Failed to get token");
const data = await response.json();
setToken(data.token);
} catch (err) {
setError(err instanceof Error ? err.message : "Error");
}
}
fetchToken();
}, [roomName, participantName]);
if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="text-center">
<p className="text-red-400 text-lg mb-4">{error}</p>
<button
onClick={onLeave}
className="px-6 py-2 bg-gray-700 text-white rounded-lg"
>
العودة
</button>
</div>
</div>
);
}
if (!token) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="text-white">جارٍ الاتصال...</div>
</div>
);
}
return (
<LiveKitRoom
token={token}
serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL}
connect={true}
onDisconnected={onLeave}
data-lk-theme="default"
style={{ height: "100vh" }}
>
<div className="flex flex-col h-screen bg-gray-950">
<StageArea />
<CustomControlBar />
</div>
<RoomAudioRenderer />
</LiveKitRoom>
);
}الفرق بين هذا المكون والمكون الجاهز:
StageArea— يستخدمuseTracksلجلب مسارات الكاميرا ومشاركة الشاشة، ويعرضها في شبكة عبرGridLayoutCustomControlBar— شريط أدوات مخصص بالكامل مع عدّاد المشاركين وأزرار تحكم مُنسقةTrackToggle— مكون جاهز يُبدّل حالة المسار (تشغيل/إيقاف) مع تحديث الأيقونة تلقائيًا
الخطوة 9: إضافة الأحداث ومعالجة حالات الاتصال
لإضافة معالجة أحداث الغرفة وتتبع حالة المشاركين، أنشئ ملف src/hooks/useRoomEvents.ts:
import { useEffect } from "react";
import { useRoomContext } from "@livekit/components-react";
import { RoomEvent, ConnectionState } from "livekit-client";
export function useRoomEvents() {
const room = useRoomContext();
useEffect(() => {
function handleParticipantConnected(participant: any) {
console.log(`${participant.identity} joined the room`);
}
function handleParticipantDisconnected(participant: any) {
console.log(`${participant.identity} left the room`);
}
function handleConnectionStateChanged(state: ConnectionState) {
console.log(`Connection state: ${state}`);
}
room.on(RoomEvent.ParticipantConnected, handleParticipantConnected);
room.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected);
room.on(
RoomEvent.ConnectionStateChanged,
handleConnectionStateChanged
);
return () => {
room.off(RoomEvent.ParticipantConnected, handleParticipantConnected);
room.off(
RoomEvent.ParticipantDisconnected,
handleParticipantDisconnected
);
room.off(
RoomEvent.ConnectionStateChanged,
handleConnectionStateChanged
);
};
}, [room]);
}يمكنك استخدام هذا الخطاف داخل أي مكون محاط بـ LiveKitRoom:
function StageArea() {
useRoomEvents(); // تتبع الأحداث
const tracks = useTracks([
{ source: Track.Source.Camera, withPlaceholder: true },
{ source: Track.Source.ScreenShare, withPlaceholder: false },
]);
return (
<GridLayout tracks={tracks}>
<ParticipantTile />
</GridLayout>
);
}أحداث الغرفة الرئيسية في LiveKit:
| الحدث | الوصف |
|---|---|
ParticipantConnected | مشارك جديد انضم |
ParticipantDisconnected | مشارك غادر |
TrackSubscribed | بدء استقبال مسار وسائط |
TrackUnsubscribed | توقف استقبال مسار |
ConnectionStateChanged | تغيرت حالة الاتصال |
DataReceived | استقبال رسالة بيانات |
ActiveSpeakersChanged | تغيّر المتحدثون النشطون |
الخطوة 10: إرسال واستقبال الرسائل النصية
LiveKit يدعم إرسال البيانات بين المشاركين عبر قناة البيانات (Data Channel). أنشئ ملف src/components/Chat.tsx:
"use client";
import { useState, useEffect, useRef, FormEvent } from "react";
import { useRoomContext } from "@livekit/components-react";
import { DataPacket_Kind, RoomEvent } from "livekit-client";
interface ChatMessage {
sender: string;
text: string;
timestamp: number;
}
export function Chat() {
const room = useRoomContext();
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState("");
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleDataReceived(
payload: Uint8Array,
participant: any
) {
const text = new TextDecoder().decode(payload);
const message: ChatMessage = {
sender: participant?.identity || "Unknown",
text,
timestamp: Date.now(),
};
setMessages((prev) => [...prev, message]);
}
room.on(RoomEvent.DataReceived, handleDataReceived);
return () => {
room.off(RoomEvent.DataReceived, handleDataReceived);
};
}, [room]);
useEffect(() => {
scrollRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
async function sendMessage(e: FormEvent) {
e.preventDefault();
if (!input.trim()) return;
const data = new TextEncoder().encode(input.trim());
await room.localParticipant.publishData(data, {
reliable: true,
});
setMessages((prev) => [
...prev,
{
sender: room.localParticipant.identity,
text: input.trim(),
timestamp: Date.now(),
},
]);
setInput("");
}
return (
<div className="w-80 bg-gray-900 border-l border-gray-800 flex flex-col">
<div className="p-4 border-b border-gray-800">
<h3 className="text-white font-semibold">الدردشة</h3>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{messages.map((msg, i) => (
<div key={i} className="text-sm">
<span className="font-medium text-blue-400">
{msg.sender}:
</span>{" "}
<span className="text-gray-300">{msg.text}</span>
</div>
))}
<div ref={scrollRef} />
</div>
<form
onSubmit={sendMessage}
className="p-4 border-t border-gray-800"
>
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="اكتب رسالة..."
className="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white text-sm placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700"
>
إرسال
</button>
</div>
</form>
</div>
);
}هذا المكون يضيف دردشة نصية داخل الغرفة:
publishData— يُرسل البيانات لجميع المشاركين عبر قناة بيانات WebRTCDataReceived— حدث يُطلق عند استقبال بيانات من مشارك آخرreliable: true— يستخدم قناة بيانات موثوقة (مثل TCP) لضمان وصول الرسائل
لدمج الدردشة مع غرفة الفيديو المخصصة:
<LiveKitRoom token={token} serverUrl={url} connect={true}>
<div className="flex h-screen bg-gray-950">
<div className="flex-1 flex flex-col">
<StageArea />
<CustomControlBar />
</div>
<Chat />
</div>
<RoomAudioRenderer />
</LiveKitRoom>الخطوة 11: إضافة إعدادات ما قبل الانضمام
تجربة مستخدم أفضل تتضمن معاينة الكاميرا والميكروفون قبل الانضمام. أنشئ ملف src/components/PreJoin.tsx:
"use client";
import { useState, useEffect, useRef } from "react";
interface PreJoinProps {
onJoin: (settings: {
videoEnabled: boolean;
audioEnabled: boolean;
}) => void;
participantName: string;
}
export function PreJoin({ onJoin, participantName }: PreJoinProps) {
const [videoEnabled, setVideoEnabled] = useState(true);
const [audioEnabled, setAudioEnabled] = useState(true);
const [stream, setStream] = useState<MediaStream | null>(null);
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
async function getMedia() {
try {
const mediaStream =
await navigator.mediaDevices.getUserMedia({
video: videoEnabled,
audio: audioEnabled,
});
setStream(mediaStream);
if (videoRef.current) {
videoRef.current.srcObject = mediaStream;
}
} catch (err) {
console.error("Failed to access media devices:", err);
}
}
getMedia();
return () => {
stream?.getTracks().forEach((track) => track.stop());
};
}, [videoEnabled, audioEnabled]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="w-full max-w-lg p-8 bg-gray-900 rounded-2xl">
<h2 className="text-xl font-bold text-white text-center mb-6">
الاستعداد للانضمام
</h2>
<div className="aspect-video bg-gray-800 rounded-xl overflow-hidden mb-6 relative">
{videoEnabled ? (
<video
ref={videoRef}
autoPlay
muted
playsInline
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<div className="w-20 h-20 bg-gray-700 rounded-full flex items-center justify-center">
<span className="text-2xl text-white">
{participantName[0]?.toUpperCase()}
</span>
</div>
</div>
)}
</div>
<div className="flex justify-center gap-4 mb-6">
<button
onClick={() => setAudioEnabled(!audioEnabled)}
className={`px-4 py-2 rounded-full transition-colors ${
audioEnabled
? "bg-gray-700 text-white"
: "bg-red-600 text-white"
}`}
>
{audioEnabled ? "🎤 الميكروفون مفعّل" : "🎤 الميكروفون معطّل"}
</button>
<button
onClick={() => setVideoEnabled(!videoEnabled)}
className={`px-4 py-2 rounded-full transition-colors ${
videoEnabled
? "bg-gray-700 text-white"
: "bg-red-600 text-white"
}`}
>
{videoEnabled ? "📷 الكاميرا مفعّلة" : "📷 الكاميرا معطّلة"}
</button>
</div>
<button
onClick={() => {
stream?.getTracks().forEach((track) => track.stop());
onJoin({ videoEnabled, audioEnabled });
}}
className="w-full py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-colors"
>
انضم الآن
</button>
</div>
</div>
);
}هذا المكون يعرض معاينة الكاميرا ويسمح للمستخدم بتشغيل أو إيقاف الميكروفون والكاميرا قبل الانضمام للغرفة.
الخطوة 12: تشغيل التطبيق واختباره
شغّل خادم التطوير:
npm run devافتح المتصفح على http://localhost:3000 واتبع هذه الخطوات:
- أدخل اسمك واسم غرفة (مثلاً "test-room")
- اضغط "انضم للغرفة"
- اسمح بالوصول للكاميرا والميكروفون عند الطلب
- لاختبار المكالمة، افتح نافذة متصفح ثانية (أو متصفح آخر) وانضم لنفس الغرفة باسم مختلف
يجب أن ترى بث الفيديو لكلا المشاركين وتسمع الصوت.
استكشاف الأخطاء الشائعة
| المشكلة | الحل |
|---|---|
| خطأ في الاتصال | تأكد من صحة LIVEKIT_URL و NEXT_PUBLIC_LIVEKIT_URL |
| لا يظهر فيديو | تأكد من منح صلاحية الكاميرا في المتصفح |
| لا يعمل الصوت | تأكد من منح صلاحية الميكروفون |
| خطأ CORS | إذا كنت تستخدم خادم محلي، تأكد من تشغيله على المنافذ الصحيحة |
الخطوة 13: نشر التطبيق في بيئة الإنتاج
النشر على Vercel
- ادفع الكود لمستودع Git:
git init
git add .
git commit -m "feat: livekit video app"
git remote add origin https://github.com/your-username/livekit-video.git
git push -u origin main-
اربط المستودع في Vercel
-
أضف متغيرات البيئة في إعدادات المشروع:
NEXT_PUBLIC_LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secret
- انشر التطبيق
النشر بـ Docker
أنشئ ملف Dockerfile:
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]ثم ابنِ وشغّل:
docker build -t livekit-video .
docker run -p 3000:3000 --env-file .env.local livekit-videoميزات متقدمة
تسجيل المكالمات
LiveKit يدعم تسجيل المكالمات عبر Egress API:
import { EgressClient, EncodedFileOutput } from "livekit-server-sdk";
const egressClient = new EgressClient(
process.env.LIVEKIT_URL!,
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!
);
// بدء تسجيل غرفة
const output = new EncodedFileOutput({
filepath: "recordings/room-{room_name}-{time}.mp4",
// إعداد S3 أو GCS للتخزين
});
await egressClient.startRoomCompositeEgress(roomName, { file: output });وكيل ذكاء اصطناعي صوتي
يمكنك بناء وكيل AI صوتي ينضم للغرفة ويستجيب بالصوت باستخدام LiveKit Agents:
# agents/voice_agent.py (Python SDK)
from livekit.agents import AutoSubscribe, JobContext, WorkerOptions, cli
from livekit.agents.voice_assistant import VoiceAssistant
from livekit.plugins import openai, silero
async def entrypoint(ctx: JobContext):
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
assistant = VoiceAssistant(
vad=silero.VAD.load(),
stt=openai.STT(),
llm=openai.LLM(),
tts=openai.TTS(),
)
assistant.start(ctx.room)
await assistant.say("مرحبًا! كيف يمكنني مساعدتك؟")هذا يفتح إمكانيات هائلة لبناء مساعدين صوتيين تفاعليين وروبوتات دعم عملاء وأدوات تعليمية صوتية.
الخطوات التالية
بعد إكمال هذا الدليل، يمكنك:
- إضافة المصادقة — استخدم NextAuth أو Better Auth لحماية الغرف
- إضافة غرف الانتظار — اجعل المضيف يقبل المشاركين قبل دخولهم
- بناء وكيل AI — استخدم LiveKit Agents لإضافة مساعد صوتي ذكي
- إضافة تسجيل — سجّل المكالمات وخزنها في S3
- تحسين الأداء — استخدم Simulcast لتكييف الجودة مع سرعة الشبكة
- إضافة السبورة البيضاء — ادمج أداة رسم تعاونية في الغرفة
الخلاصة
في هذا الدليل، بنينا تطبيق مؤتمرات فيديو كامل باستخدام LiveKit و Next.js. تعلمنا كيفية إنشاء رموز الوصول من الخادم، بناء واجهة فيديو باستخدام مكونات React الجاهزة، تخصيص شريط الأدوات، إضافة دردشة نصية عبر قنوات البيانات، وبناء شاشة معاينة قبل الانضمام.
LiveKit يُبسّط بشكل كبير بناء تطبيقات الفيديو والصوت في الوقت الحقيقي. بنيته المفتوحة المصدر ومكتبات React الجاهزة تجعل من السهل البدء سريعًا، بينما واجهاته المتقدمة (Agents، Egress، Ingress) تمنحك القدرة على بناء أي شيء من مؤتمرات فيديو بسيطة إلى وكلاء ذكاء اصطناعي صوتيين متطورين.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء تطبيق كامل مع Firebase و Next.js 15: المصادقة، Firestore والتحديث الفوري
تعلم كيفية بناء تطبيق full-stack مع Next.js 15 و Firebase. يغطي هذا الدليل المصادقة، Firestore، التحديثات الفورية، Server Actions والنشر على Vercel.

بناء وكيل ذكاء اصطناعي مستقل باستخدام Agentic RAG و Next.js
تعلم كيف تبني وكيل ذكاء اصطناعي يقرر بشكل مستقل متى وكيف يسترجع المعلومات من قواعد البيانات المتجهية. دليل عملي شامل باستخدام Vercel AI SDK و Next.js مع أمثلة قابلة للتنفيذ.

بناء محرك بحث دلالي بالذكاء الاصطناعي مع Next.js 15 و OpenAI و Pinecone
تعلّم كيف تبني محرك بحث دلالي متقدّم باستخدام Next.js 15 و OpenAI Embeddings و قاعدة بيانات Pinecone المتجهية. يغطي هذا الدليل الشامل من الإعداد إلى النشر مع Server Actions وواجهة بحث تفاعلية.