WebSockets والتطبيقات الفورية
أساسيات WebRTC
أساسيات WebRTC
WebRTC (الاتصال في الوقت الفعلي عبر الويب) هي تقنية قوية تتيح الاتصال الصوتي والمرئي ونقل البيانات من نظير إلى نظير مباشرة بين المتصفحات دون الحاجة إلى مكونات إضافية أو خوادم وسيطة.
ما هو WebRTC؟
يتكون WebRTC من عدة واجهات برمجة تطبيقات وبروتوكولات رئيسية:
- MediaStream API (getUserMedia): الوصول إلى الكاميرا والميكروفون
- RTCPeerConnection: بث الصوت/الفيديو بين النظراء
- RTCDataChannel: إرسال بيانات عشوائية بين النظراء
- الإشارات: تبادل معلومات الاتصال (ليست جزءًا من مواصفات WebRTC)
مفهوم أساسي: يتيح WebRTC الاتصال المباشر من نظير إلى نظير، مما يعني أن البيانات تتدفق مباشرة بين المتصفحات دون المرور عبر خادم (بعد إعداد الاتصال الأولي).
الاتصال من نظير إلى نظير
تدفق اتصال WebRTC:
1. ينشئ المستخدم A عرضًا (SDP - بروتوكول وصف الجلسة)
2. يرسل المستخدم A العرض إلى المستخدم B عبر خادم الإشارات
3. يستقبل المستخدم B العرض وينشئ إجابة
4. يرسل المستخدم B الإجابة مرة أخرى إلى المستخدم A عبر خادم الإشارات
5. يتبادل كلا المستخدمين مرشحي ICE للعثور على أفضل مسار اتصال
6. يتم إنشاء اتصال مباشر من نظير إلى نظير
7. تتدفق تدفقات الوسائط (الصوت/الفيديو) أو البيانات مباشرة بين النظراء
الوصول إلى الكاميرا والميكروفون
تسمح واجهة getUserMedia API بالوصول إلى أجهزة الوسائط:
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>الوصول إلى الكاميرا WebRTC</title>
</head>
<body>
<video id="localVideo" autoplay playsinline muted></video>
<button id="startBtn">تشغيل الكاميرا</button>
<button id="stopBtn">إيقاف الكاميرا</button>
<script>
const localVideo = document.getElementById('localVideo');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
let localStream = null;
// تشغيل الكاميرا والميكروفون
startBtn.addEventListener('click', async () => {
try {
// طلب الوصول إلى الكاميرا والميكروفون
localStream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user' // الكاميرا الأمامية
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
// عرض تدفق الفيديو
localVideo.srcObject = localStream;
console.log('تم تشغيل الكاميرا والميكروفون');
} catch (error) {
console.error('خطأ في الوصول إلى أجهزة الوسائط:', error);
alert('تعذر الوصول إلى الكاميرا/الميكروفون: ' + error.message);
}
});
// إيقاف جميع المسارات
stopBtn.addEventListener('click', () => {
if (localStream) {
localStream.getTracks().forEach(track => {
track.stop();
console.log('تم إيقاف المسار:', track.kind);
});
localVideo.srcObject = null;
localStream = null;
}
});
</script>
</body>
</html>
معالجة الأذونات: تتطلب المتصفحات HTTPS لـ getUserMedia (باستثناء localhost). تعامل دائمًا مع رفض الإذن بشكل سلس مع رسائل خطأ سهلة الاستخدام.
خادم الإشارات
يتطلب WebRTC خادم إشارات لتبادل معلومات الاتصال. إليك خادم إشارات بسيط يعتمد على WebSocket:
// signaling-server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
const clients = new Map();
wss.on('connection', (ws) => {
const clientId = generateId();
clients.set(clientId, ws);
console.log(`العميل ${clientId} متصل`);
// إرسال معرف العميل
ws.send(JSON.stringify({
type: 'registered',
clientId: clientId
}));
ws.on('message', (message) => {
const data = JSON.parse(message);
switch (data.type) {
case 'offer':
case 'answer':
case 'ice-candidate':
// إعادة التوجيه إلى العميل المستهدف
const targetWs = clients.get(data.target);
if (targetWs && targetWs.readyState === WebSocket.OPEN) {
targetWs.send(JSON.stringify({
...data,
from: clientId
}));
}
break;
case 'list-clients':
// إرسال قائمة العملاء المتصلين
ws.send(JSON.stringify({
type: 'clients-list',
clients: Array.from(clients.keys()).filter(id => id !== clientId)
}));
break;
}
});
ws.on('close', () => {
clients.delete(clientId);
console.log(`العميل ${clientId} منفصل`);
});
});
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
console.log('خادم الإشارات يعمل على المنفذ 8080');
مرشحو ICE
يجد ICE (إنشاء الاتصال التفاعلي) أفضل مسار لتوصيل النظراء:
// أنواع مرشحي ICE:
// 1. مرشح المضيف - عنوان IP المحلي المباشر
{
candidate: 'candidate:1 1 UDP 2130706431 192.168.1.100 54321 typ host',
sdpMLineIndex: 0,
sdpMid: '0'
}
// 2. مرشح انعكاس الخادم - IP عام من خادم STUN
{
candidate: 'candidate:2 1 UDP 1694498815 203.0.113.1 54321 typ srflx',
sdpMLineIndex: 0,
sdpMid: '0'
}
// 3. مرشح الترحيل - ترحيل خادم TURN
{
candidate: 'candidate:3 1 UDP 16777215 198.51.100.1 54321 typ relay',
sdpMLineIndex: 0,
sdpMid: '0'
}
خوادم STUN و TURN
تساعد خوادم اجتياز الشبكة في إنشاء اتصالات عبر جدران الحماية و NAT:
- STUN (أدوات اجتياز الجلسة لـ NAT): اكتشاف عنوان IP العام والمنفذ
- TURN (الاجتياز باستخدام الترحيلات حول NAT): ترحيل حركة المرور عند فشل الاتصال المباشر
// تكوين خوادم ICE
const configuration = {
iceServers: [
{
// خادم STUN العام من Google
urls: 'stun:stun.l.google.com:19302'
},
{
// خوادم STUN العامة
urls: [
'stun:stun1.l.google.com:19302',
'stun:stun2.l.google.com:19302'
]
},
{
// خادم TURN (يتطلب مصادقة)
urls: 'turn:turn.example.com:3478',
username: 'user',
credential: 'password'
}
],
iceCandidatePoolSize: 10
};
const peerConnection = new RTCPeerConnection(configuration);
ملاحظة الإنتاج: خوادم STUN من Google مجانية ولكنها غير مضمونة للإنتاج. للتطبيقات الحرجة، قم بتشغيل خوادم STUN/TURN الخاصة بك (على سبيل المثال، coturn).
أساسيات RTCPeerConnection
إنشاء وإدارة اتصال نظير:
// إنشاء اتصال نظير
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};
const peerConnection = new RTCPeerConnection(configuration);
// إضافة مسارات التدفق المحلي إلى الاتصال
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
// الاستماع للتدفق البعيد
peerConnection.ontrack = (event) => {
console.log('تم استقبال مسار بعيد');
const remoteVideo = document.getElementById('remoteVideo');
if (!remoteVideo.srcObject) {
remoteVideo.srcObject = event.streams[0];
}
};
// الاستماع لمرشحي ICE
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
// إرسال المرشح إلى النظير البعيد عبر الإشارات
sendToSignaling({
type: 'ice-candidate',
candidate: event.candidate,
target: remotePeerId
});
}
};
// الاستماع لتغييرات حالة الاتصال
peerConnection.onconnectionstatechange = () => {
console.log('حالة الاتصال:', peerConnection.connectionState);
switch (peerConnection.connectionState) {
case 'connected':
console.log('النظراء متصلون!');
break;
case 'disconnected':
console.log('النظراء منفصلون');
break;
case 'failed':
console.log('فشل الاتصال');
break;
case 'closed':
console.log('تم إغلاق الاتصال');
break;
}
};
// الاستماع لحالة اتصال ICE
peerConnection.oniceconnectionstatechange = () => {
console.log('حالة ICE:', peerConnection.iceConnectionState);
};
إنشاء عرض
ينشئ النظير المتصل ويرسل عرضًا:
async function createOffer() {
try {
// إنشاء عرض
const offer = await peerConnection.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
});
// تعيين الوصف المحلي
await peerConnection.setLocalDescription(offer);
console.log('تم إنشاء العرض:', offer);
// إرسال العرض إلى النظير البعيد عبر الإشارات
sendToSignaling({
type: 'offer',
sdp: offer,
target: remotePeerId
});
} catch (error) {
console.error('خطأ في إنشاء العرض:', error);
}
}
معالجة عرض وإنشاء إجابة
يتعامل النظير المستقبل مع العرض ويستجيب بإجابة:
async function handleOffer(offer, from) {
try {
// تعيين الوصف البعيد من العرض
await peerConnection.setRemoteDescription(
new RTCSessionDescription(offer)
);
console.log('تم تعيين الوصف البعيد من العرض');
// إنشاء إجابة
const answer = await peerConnection.createAnswer();
// تعيين الوصف المحلي
await peerConnection.setLocalDescription(answer);
console.log('تم إنشاء الإجابة:', answer);
// إرسال الإجابة مرة أخرى إلى المتصل عبر الإشارات
sendToSignaling({
type: 'answer',
sdp: answer,
target: from
});
} catch (error) {
console.error('خطأ في معالجة العرض:', error);
}
}
async function handleAnswer(answer) {
try {
// تعيين الوصف البعيد من الإجابة
await peerConnection.setRemoteDescription(
new RTCSessionDescription(answer)
);
console.log('تم تعيين الوصف البعيد من الإجابة');
} catch (error) {
console.error('خطأ في معالجة الإجابة:', error);
}
}
إضافة مرشحي ICE
تبادل مرشحي ICE للعثور على أفضل مسار اتصال:
async function handleIceCandidate(candidate) {
try {
await peerConnection.addIceCandidate(
new RTCIceCandidate(candidate)
);
console.log('تم إضافة مرشح ICE');
} catch (error) {
console.error('خطأ في إضافة مرشح ICE:', error);
}
}
مثال WebRTC بسيط كامل
// مثال كامل من جانب العميل
const signalingServer = new WebSocket('ws://localhost:8080');
let peerConnection = null;
let localStream = null;
let remotePeerId = null;
signalingServer.onopen = () => {
console.log('متصل بخادم الإشارات');
};
signalingServer.onmessage = async (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'registered':
console.log('معرفي:', data.clientId);
break;
case 'offer':
remotePeerId = data.from;
await handleOffer(data.sdp, data.from);
break;
case 'answer':
await handleAnswer(data.sdp);
break;
case 'ice-candidate':
await handleIceCandidate(data.candidate);
break;
}
};
function sendToSignaling(message) {
signalingServer.send(JSON.stringify(message));
}
async function startCall(targetId) {
remotePeerId = targetId;
// الحصول على الوسائط المحلية
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
document.getElementById('localVideo').srcObject = localStream;
// إنشاء اتصال نظير
peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// إضافة المسارات
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
// معالجة التدفق البعيد
peerConnection.ontrack = (event) => {
document.getElementById('remoteVideo').srcObject = event.streams[0];
};
// معالجة مرشحي ICE
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
sendToSignaling({
type: 'ice-candidate',
candidate: event.candidate,
target: remotePeerId
});
}
};
// إنشاء وإرسال العرض
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
sendToSignaling({
type: 'offer',
sdp: offer,
target: targetId
});
}
التمرين: قم ببناء دردشة فيديو بسيطة
قم بإنشاء دردشة فيديو أساسية لشخصين:
- قم بإعداد خادم إشارات باستخدام WebSockets (استخدم المثال أعلاه)
- قم بإنشاء صفحة ويب بعناصر فيديو محلية وبعيدة
- قم بتنفيذ getUserMedia للوصول إلى الكاميرا والميكروفون
- قم بإنشاء RTCPeerConnection ومعالجة تدفق العرض/الإجابة
- تبادل مرشحي ICE بين النظراء
- اعرض حالة الاتصال (الاتصال، متصل، فشل)
- أضف عناصر تحكم أساسية: كتم/إلغاء كتم الصوت، تمكين/تعطيل الفيديو
- إضافي: أضف زر \"إنهاء المكالمة\" لإغلاق الاتصال بشكل صحيح
الملخص
- يتيح WebRTC الاتصال الصوتي والمرئي ونقل البيانات من نظير إلى نظير
- يصل getUserMedia إلى الكاميرا والميكروفون بإذن المستخدم
- يدير RTCPeerConnection اتصال النظير إلى النظير
- تتبادل خوادم الإشارات معلومات الاتصال (ليست جزءًا من مواصفات WebRTC)
- تساعد مرشحو ICE في إيجاد أفضل مسار اتصال عبر NAT/جدران الحماية
- تكتشف خوادم STUN عناوين IP العامة
- تقوم خوادم TURN بترحيل حركة المرور عند فشل الاتصال المباشر
- يحدد نموذج العرض/الإجابة معلمات الاتصال
- يتطلب WebRTC HTTPS في الإنتاج (باستثناء localhost)