تفعيل التواصل في الوقت الفعلي باستخدام WebRTC

1. قبل البدء

يعلّمك هذا الدرس التطبيقي حول الترميز كيفية إنشاء تطبيق للحصول على فيديو والتقاط لقطات باستخدام كاميرا الويب، ومشاركته باستخدام شبكة الند للند مع WebRTC. ويمكنك أيضًا معرفة كيفية استخدام واجهات برمجة تطبيقات WebRTC الأساسية وإعداد خادم مراسلة باستخدام Node.js.

المتطلّبات الأساسية

  • معرفة أساسية بلغة HTML وCSS وJavaScript

العناصر التي سيتم إنشاؤها

  • يمكنك الحصول على فيديو من كاميرا الويب.
  • يمكنك بث الفيديو باستخدام RTCPeerConnection.
  • يمكنك بث البيانات باستخدام RTCDataChannel.
  • يمكنك إعداد خدمة إشارة لتبادل الرسائل.
  • الجمع بين اتصال النظير والإشارات.
  • التقِط صورة واستخدِم قناة بيانات لمشاركتها.

الأشياء التي تحتاج إليها

  • الإصدار 47 من Chrome أو الإصدارات الأحدث
  • خادم ويب لمتصفح Chrome أو خادم ويب من اختيارك
  • محرر نصوص من اختيارك
  • Node.js

2. الحصول على نموذج الرمز

تنزيل الرمز

  1. إذا كنت معتادًا على Git، شغِّل هذا الأمر لنسخ نسخة من رمز هذا الدرس التطبيقي من GitHub:
git clone https://github.com/googlecodelabs/webrtc-web

أو انقر على هذا الرابط لتنزيل ملف ZIP للرمز:

  1. افتح ملف ZIP الذي تم تنزيله لفك ضغط مجلد المشروع الذي يحمل اسم webrtc-web-master، والذي يحتوي على مجلد واحد لكل خطوة في هذا الدرس التطبيقي وجميع الموارد التي تحتاج إليها.

يمكنك استخدام كل الرموز على الدليل المسمى work.

تحتوي مجلدات step-nn على نسخة مكتملة لكل خطوة من هذا الدرس التطبيقي حول الترميز. وهي جاهزة كمرجع.

تثبيت خادم الويب وإثبات ملكيته

على الرغم من أن بإمكانك استخدام خادم الويب الخاص بك، فإن هذا الدرس التطبيقي المُصمَّم للعمل بشكلٍ جيد مع Web Server for Chrome.

  1. إذا لم يكن لديك خادم الويب لمتصفح Chrome، انقر على هذا الرابط لتثبيته من سوق Chrome الإلكتروني:

d0a4649b4920cf3.png

  1. انقر على إضافة إلى Chrome الذي يثبّت خادم الويب لمتصفح Chrome ويفتح تطبيقات Google تلقائيًا في علامة تبويب جديدة.
  2. انقر على خادم الويب:

27fce4494f641883.png

سيظهر مربع حوار يتيح لك تهيئة خادم الويب المحلي:

a300381a486b9e22.png

  1. انقر على اختيار مجلد.
  2. اختَر مجلد work الذي أنشأته.

ضمن عناوين URL لخادم الويب، سيظهر لك عنوان URL الذي يمكنك من خلاله عرض عملك قيد التقدّم في

Chrome.

  1. ضمن الخيارات (قد تحتاج إلى إعادة التشغيل)، ضع علامة في مربّع الاختيار عرض index.html تلقائيًا.
  2. بدِّل بين خادم الويب: تم البدء مرتين لإيقاف الخادم وإعادة تشغيله.

f23cafb3993dfac1.png

  1. انقر على عنوان URL ضمن عناوين URL لخادم الويب لعرض عملك في متصفح الويب.

من المفترض أن تظهر صفحة تمثّل work/index.html.

18a705cb6ccc5181.png

من الواضح أن هذا التطبيق لا تفعل أي شيء مثير للاهتمام حتى الآن. إنه هيكل عظمي فقط لضمان عمل خادم الويب بشكل صحيح. ويمكنك إضافة ميزات الوظائف والتنسيق في الخطوات اللاحقة.

3- بث الفيديو باستخدام كاميرا الويب

تتوفّر نسخة كاملة من هذه الخطوة في مجلد step-01.

إضافة شرطة HTML

انسخ هذا الرمز والصقه في ملف index.html في دليل work لإضافة عنصر video وscript:

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <video autoplay playsinline></video>

  <script src="js/main.js"></script>

</body>

</html>

إضافة إمكانية استخدام JavaScript

يمكنك نسخ هذا الرمز ولصقه في ملف main.js في مجلد js:

'use strict';

// In this codelab, you  only stream video, not audio (video: true).
const mediaStreamConstraints = {
  video: true,
};

// The video element where the stream is displayed
const localVideo = document.querySelector('video');

// The local stream that's displayed on the video
let localStream;

// Handle success and add the MediaStream to the video element
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream;
  localVideo.srcObject = mediaStream;
}

// Handle error and log a message to the console with the error message
function handleLocalMediaStreamError(error) {
  console.log('navigator.getUserMedia error: ', error);
}

// Initialize media stream
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);

تجربة ذلك

افتح ملف index.html في المتصفح ويُفترض أن يظهر لك المحتوى التالي، ولكن بالطبع من كاميرا الويب:

9297048e43ed0f3d.png

آلية العمل

بعد طلب getUserMedia()، يطلب المتصفّح إذن الوصول إلى الكاميرا إذا كان هذا أول طلب للوصول إلى الكاميرا للأصل الحالي.

إذا تم ذلك بنجاح، سيتم عرض MediaStream، بحيث يمكن للعنصر media استخدامه من خلال السمة srcObject:

navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);


}
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

تتيح لك الوسيطة constraints تحديد الوسائط التي سيتم الحصول عليها. في هذا المثال، تكون الوسائط عبارة عن فيديو فقط لأن الصوت غير مفعّل بشكل تلقائي:

const mediaStreamConstraints = {
  video: true,
};

يمكنك استخدام قيود معيّنة لاستيفاء المتطلبات الإضافية، مثل درجة دقة الفيديو:

const hdConstraints = {
  video: {
    width: {
      min: 1280
    },
    height: {
      min: 720
    }
  }
}

تسرد مواصفات MediaTrackConstraints جميع أنواع القيود المحتملة، ولكن ليست جميع الخيارات متوافقة مع جميع المتصفحات. إذا لم تكن درجة الدقة المطلوبة متوافقة مع الكاميرا المحددة حاليًا، يتم رفض getUserMedia() مع OverconstrainedError وسيُطلب منك منح الإذن بالوصول إلى الكاميرا.

في حال نجاح getUserMedia()، يتم ضبط بث الفيديو من كاميرا الويب كمصدر لعنصر الفيديو:

function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

الحصول على نقاط إضافية

  • الكائن localStream الذي تم تمريره إلى getUserMedia() موجود في النطاق العام، لذا يمكنك فحصه من وحدة تحكم المتصفح. افتح وحدة التحكّم واكتب stream, واضغط على Enter (Return على Mac). للاطِّلاع على وحدة التحكُّم في Chrome، اضغط على Control+Shift+J (أو Command+Option+J على نظام التشغيل Mac).
  • ما الذي تعرضه localStream.getVideoTracks()؟
  • تواصل هاتفيًا مع "localStream.getVideoTracks()[0].stop()".
  • انظر إلى كائن القيود. ما الذي يحدث عند تغييره إلى {audio: true, video: true}؟
  • ما هو حجم عنصر الفيديو؟ كيف يمكنك معرفة حجم الفيديو الطبيعي من JavaScript مقارنةً بحجم العرض؟ استخدم أدوات المطورين في Google Chrome للتحقق.
  • أضف فلاتر CSS إلى عنصر الفيديو، كما يلي:
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • أضِف فلاتر SVG، مثل ما يلي:
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

نصائح

أفضل ممارسة

تأكد من أن عنصر الفيديو لا يتجاوز الحاوية. أضاف هذا الدرس التطبيقي الترميز width وmax-width لضبط حجم مفضّل وحد أقصى لحجم الفيديو. يحسب المتصفّح الارتفاع تلقائيًا.

video {
  max-width: 100%;
  width: 320px;
}

4. بث الفيديو باستخدام واجهة برمجة تطبيقات RTCPeerConnection

تتوفّر نسخة كاملة من هذه الخطوة في مجلد step-2.

إضافة عناصر الفيديو وأزرار التحكم

في ملف index.html، استبدل عنصر video الفردي بعنصرين video وثلاثة عناصر button:

<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>


<div>
  <button id="startButton">Start</button>
  <button id="callButton">Call</button>
  <button id="hangupButton">Hang Up</button>
</div>

يعرض أحدهما فيديو البث من getUserMedia()، في حين يعرض الآخر الفيديو نفسه الذي تم بثه من خلال RTCPeerconnection. (في الواقع الفعلي، سيعرض عنصر video ساحة المشاركات المحلية، بينما يعرض العنصر الآخر البث البعيد).

إضافة إضاءة محوّل js.

انسخ عنصر النص البرمجي هذا والصقه أعلى عنصر النص البرمجي في main.js:

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

من المفترض أن يظهر ملف index.html على النحو التالي:

<!DOCTYPE html>
<html>

<head>
  <title>Real-time communication with WebRTC</title>
  <link rel="stylesheet" href="css/main.css" />
</head>

<body>
  <h1>Real-time communication with WebRTC</h1>

  <video id="localVideo" autoplay playsinline></video>
  <video id="remoteVideo" autoplay playsinline></video>

  <div>
    <button id="startButton">Start</button>
    <button id="callButton">Call</button>
    <button id="hangupButton">Hang Up</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

تثبيت رمز RTCPeerConnection

استبدل main.js بالنسخة في مجلد step-02.

إجراء المكالمة

  1. افتح ملف index.html.
  2. انقر على بدء للحصول على فيديو من كاميرا الويب.
  3. انقر على اتصال لإجراء الاتصال بين الزملاء.

من المفترض أن يظهر الفيديو نفسه من كاميرا الويب في عنصري video.

  1. عرض وحدة تحكم المتصفح للاطلاع على تسجيل WebRTC.

آلية العمل

هذه الخطوة مفيدة جدًا.

تستخدم تقنية WebRTC واجهة برمجة التطبيقات RTCPeerConnection لإعداد اتصال لبث الفيديو بين برامج WebRTC، المعروفة باسم التطبيقات المشابهة.

في هذا المثال، يظهر العنصران RTCPeerConnection في الصفحة نفسها: pc1 وpc2.

يتضمن إعداد المكالمة بين نظراء WebRTC ثلاث مهام:

  1. أنشئ RTCPeerConnection لكل نهاية مكالمة، وأضِف البث المحلي من getUserMedia() في كل نهاية.
  2. الحصول على معلومات الشبكة ومشاركتها

تُعرف نقاط نهاية الاتصال المحتملة بالمرشحين ICE.

  1. يمكنك الحصول على أوصاف محلية وعن بُعد ومشاركتها.

تكون البيانات الوصفية للوسائط المحلية بتنسيق بروتوكول وصف الجلسة (SDP).

لنفترض أن نبيلة ويوسف يريدان استخدام RTCPeerConnection لإعداد محادثة فيديو.

أولاً، تتبادل "أليس" و"بوب" معلومات الشبكة. يشير التعبير العثور على المرشحين إلى عملية العثور على واجهات الشبكة والمنافذ باستخدام إطار عمل ICE.

  1. تنشئ نبيلة كائن RTCPeerConnection مع المعالج onicecandidate (addEventListener('icecandidate')).

يتوافق ذلك مع الرمز التالي من main.js:

let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  1. تتصل نبيلة بـ getUserMedia() وتضيف البث المباشر الذي تم تمريره إلى:
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
  then(gotLocalMediaStream).
  catch(handleLocalMediaStreamError);
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
  localStream = mediaStream;
  trace('Received local stream.');
  callButton.disabled = false;  // Enable call button.
}
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.');
  1. يتم استدعاء معالج onicecandidate من الخطوة الأولى عند توفر العناصر المحفّزة لعرض الإعلان في الشبكة.
  2. ترسل نبيلة بيانات المرشح التسلسلي إلى يوسف.

في التطبيق الحقيقي، تحدث هذه العملية، المعروفة باسم الإشارة، من خلال خدمة مراسلة. وستتعلّم كيفية إجراء ذلك في خطوة لاحقة. وبالتأكيد، في هذه الخطوة، يكون الكائنان RTCPeerConnection في الصفحة نفسها ويمكنهما التواصل مباشرةً بدون الحاجة إلى إرسال رسائل خارجية.

  1. عندما يتلقّى يوسف رسالة مرشح من نبيلة، يتصل بـ addIceCandidate() لإضافة المرشح إلى وصف النظراء عن بُعد:
function handleConnection(event) {
  const peerConnection = event.target;
  const iceCandidate = event.candidate;

  if (iceCandidate) {
    const newIceCandidate = new RTCIceCandidate(iceCandidate);
    const otherPeer = getOtherPeer(peerConnection);

    otherPeer.addIceCandidate(newIceCandidate)
      .then(() => {
        handleConnectionSuccess(peerConnection);
      }).catch((error) => {
        handleConnectionFailure(peerConnection, error);
      });

    trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
          `${event.candidate.candidate}.`);
  }
}

تحتاج نظرات WebRTC أيضًا إلى اكتشاف وتبادل معلومات الصوت والفيديو عن بُعد، مثل وسائط الدقة والترميز. تتم الإشارة إلى تبادل معلومات إعداد الوسائط مع تبادل الكائنات الثنائية الكبيرة للبيانات الوصفية، والمعروفة باسم العرض والإجابة، باستخدام تنسيق SDP.

  1. تشغّل نبيلة طريقة createOffer() RTCPeerConnection.

ويقدِّم الوعد المعروض RTCSessionDescription وصفًا محليًا للجلسة من نوع Alice&#39:

trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. وفي حال نجاح عملية الإعداد، تضبط نبيلة الوصف المحلي باستخدام setLocalDescription()، ثم ترسل وصف الجلسة إلى يوسف من خلال قناة الإشارة.
  2. يحدّد "يوسف" الوصف الذي أرسلته إليه "أليس" كوصف عن بُعد باستخدام setRemoteDescription().
  3. يدير يوسف طريقة RTCPeerConnection createAnswer() ويمرر الوصف البعيد الذي حصل عليه من نبيلة إلى أن يتم إنشاء جلسة محلية متوافقة مع رسالتها.
  4. ويقطع الوعد createAnswer() العلامة RTCSessionDescription التي يحدّدها يوسف كوصف محلي ويرسلها إلى نبيلة.
  5. عندما تحصل "أليس" على وصف جلسة "بوب"، تضبطه عن بُعد باستخدام الوصف setRemoteDescription().
// Logs offer creation and sets peer connection session descriptions
function createdOffer(description) {
  trace(`Offer from localPeerConnection:\n${description.sdp}`);

  trace('localPeerConnection setLocalDescription start.');
  localPeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection setRemoteDescription start.');
  remotePeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection createAnswer start.');
  remotePeerConnection.createAnswer()
    .then(createdAnswer)
    .catch(setSessionDescriptionError);
}

// Logs answer to offer creation and sets peer-connection session descriptions
function createdAnswer(description) {
  trace(`Answer from remotePeerConnection:\n${description.sdp}.`);

  trace('remotePeerConnection setLocalDescription start.');
  remotePeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('localPeerConnection setRemoteDescription start.');
  localPeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);
}

الحصول على نقاط إضافية

  1. انتقِل إلى chrome://webrtc-internals.

توفّر هذه الصفحة إحصاءات WebRTC وبيانات تصحيح الأخطاء. (يمكنك العثور على قائمة كاملة بعناوين URL في Chrome على chrome://about.)

  1. تصميم الصفحة باستخدام نمط CSS:
  2. وضع الفيديوهات جنبًا إلى جنب
  3. جعل الأزرار بنفس العرض بنص أكبر.
  4. تأكّد من أن التنسيق يعمل على الأجهزة الجوّالة.
  5. من وحدة تحكُّم "أدوات المطوّرين" في Chrome، اطّلِع على localStream وlocalPeerConnection وremotePeerConnection.
  6. من وحدة التحكّم، انظر إلى localPeerConnectionpc1.localDescription.

كيف يبدو تنسيق SDP؟

نصائح

  • لمزيد من المعلومات حول أداة المحوّل.js، راجع adapter.js مستودع GitHub.
  • يمكنك إلقاء نظرة على AppRTC ورمزه، وهو التطبيق الأساسي لمشروع WebRTC، والذي يتم من خلاله إجراء مكالمات WebRTC. مدة إعداد المكالمة أقل من 500 ملّي ثانية.

أفضل ممارسة

ولحماية الرمز البرمجي في المستقبل، استخدِم واجهات برمجة التطبيقات الجديدة المستندة إلى Promise وفعِّل التوافق مع المتصفّحات التي لا تتوافق مع adapter.js.

5. استخدام قناة بيانات لتبادل البيانات

تتوفّر نسخة كاملة من هذه الخطوة في مجلد step-03.

تحديث HTML

بالنسبة إلى هذه الخطوة، يمكنك استخدام قنوات بيانات WebRTC لإرسال نص بين عنصرَي textarea في الصفحة نفسها. ذلك الأمر غير مفيد، ولكنه يوضِّح كيفية استخدام WebRTC لمشاركة البيانات، بالإضافة إلى بث الفيديو.

إزالة العناصر video وbutton من index.html, واستبدالها بتنسيق HTML التالي:

<textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
<textarea id="dataChannelReceive" disabled></textarea>

<div id="buttons">
  <button id="startButton">Start</button>
  <button id="sendButton">Send</button>
  <button id="closeButton">Stop</button>
</div>

أحدهما textarea لإدخال النص، والآخر لعرض النص بالشكل الذي يتم بثه بين التطبيقات المشابهة.

من المفترض أن يظهر ملف index.html على النحو التالي:

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
  <textarea id="dataChannelReceive" disabled></textarea>

  <div id="buttons">
    <button id="startButton">Start</button>
    <button id="sendButton">Send</button>
    <button id="closeButton">Stop</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>

تحديث JavaScript

  1. استبدل main.js بمحتوى step-03/js/main.js.
  1. تجربة بث البيانات بين التطبيقات المشابهة:
  2. فتح index.html
  3. انقر على Start (بدء) لإعداد اتصال النظير.
  4. أدخل بعض النصوص في textarea على اليمين.
  5. انقر على إرسال لنقل النص باستخدام قناة بيانات WebRTC.

آلية العمل

يستخدم هذا الرمز RTCPeerConnection وRTCDataChannel لتفعيل تبادل الرسائل النصية.

جزء كبير من الرمز في هذه الخطوة هو نفسه مثال RTCPeerConnection. تحتوي دالّتا sendData() وcreateConnection() على معظم الرموز الجديدة:

function createConnection() {
  dataChannelSend.placeholder = '';
  var servers = null;
  pcConstraint = null;
  dataConstraint = null;
  trace('Using SCTP based data channels');
  // For SCTP, reliable and ordered delivery is true by default.
  // Add localConnection to global scope to make it visible
  // from the browser console.
  window.localConnection = localConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created local peer connection object localConnection');

  sendChannel = localConnection.createDataChannel('sendDataChannel',
      dataConstraint);
  trace('Created send data channel');

  localConnection.onicecandidate = iceCallback1;
  sendChannel.onopen = onSendChannelStateChange;
  sendChannel.onclose = onSendChannelStateChange;

  // Add remoteConnection to global scope to make it visible
  // from the browser console.
  window.remoteConnection = remoteConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created remote peer connection object remoteConnection');

  remoteConnection.onicecandidate = iceCallback2;
  remoteConnection.ondatachannel = receiveChannelCallback;

  localConnection.createOffer().then(
    gotDescription1,
    onCreateSessionDescriptionError
  );
  startButton.disabled = true;
  closeButton.disabled = false;
}

function sendData() {
  var data = dataChannelSend.value;
  sendChannel.send(data);
  trace('Sent Data: ' + data);
}

وتتشابه بنية RTCDataChannel مع WebSocket مع طريقة send() وحدث message.

لاحِظ استخدام dataConstraint. يمكن إعداد قنوات البيانات لتفعيل أنواع مختلفة من مشاركة البيانات، مثل منح الأولوية للتسليم الموثوق به على الأداء.

الحصول على نقاط إضافية

  1. عند استخدام SCTP، يكون البروتوكول المُستخدَم من قِبل قنوات بيانات WebRTC مفعَّلاً تلقائيًا لتسليم البيانات الموثوق به والمرتَّب. عندما تحتاج RTCDataChannel إلى توفير بيانات موثوق بها ومتى قد يكون الأداء أكثر أهمية، حتى إذا كان ذلك يعني فقدان بعض البيانات؟
  2. يمكنك استخدام CSS لتحسين تنسيق الصفحة وإضافة سمة عنصر نائب إلى dataChannelReceive textarea.
  3. اختبِر الصفحة على جهاز جوّال.

التعرف على المزيد

6- إعداد خدمة الإشارة لتبادل الرسائل

لقد تعلّمت كيفية تبادل البيانات بين التطبيقات المشابهة في الصفحة نفسها، لكن كيف يمكنك إجراء ذلك بين الأجهزة المختلفة؟ عليك أولاً إعداد قناة إشارة لتبادل رسائل البيانات الوصفية.

تتوفّر نسخة كاملة من هذه الخطوة في مجلد step-04.

لمحة عن التطبيق

يستخدم WebRTC واجهة برمجة تطبيقات JavaScript من جهة العميل، ولكن عند الاستخدام الفعلي، يتطلب أيضًا خادم إشارة (مراسلة) بالإضافة إلى خوادم STUN وTURN. يمكنك الاطّلاع على المزيد من المعلومات هنا.

في هذه الخطوة، يتم إنشاء خادم إشارات Node.js بسيط باستخدام وحدة Socket.IO Node.js ومكتبة JavaScript للمراسلة.

في هذا المثال، يتم تنفيذ الخادم (تطبيق Node.js) في index.js ويتم تنفيذ البرنامج الذي يتم تشغيله عليه (تطبيق الويب) في index.html.

يحتوي تطبيق Node.js في هذه الخطوة على مهمتين.

أولاً، تُستخدم بمثابة رسالة ترحيل:

socket.on('message', function (message) {
  log('Got message: ', message);
  socket.broadcast.emit('message', message);
});

ثانيًا، يدير غرف محادثات فيديو WebRTC:

if (numClients === 0) {
  socket.join(room);
  socket.emit('created', room, socket.id);
} else if (numClients === 1) {
  socket.join(room);
  socket.emit('joined', room, socket.id);
  io.sockets.in(room).emit('ready');
} else { // max two clients
  socket.emit('full', room);
}

يتيح تطبيق WebRTC البسيط تطبيق غرفتَين كحد أقصى لمشاركة الغرفة.

HTML وJavaScript

  1. حدِّث index.html بحيث يظهر كما يلي:
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <script src="/socket.io/socket.io.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>

لن ترى أي شيء على الصفحة في هذه الخطوة. ويتم إجراء كل عمليات التسجيل في "وحدة تحكُّم المتصفِّح". للاطِّلاع على وحدة التحكُّم في Chrome، اضغط على Control+Shift+J (أو Command+Option+J على نظام التشغيل Mac).

  1. استبدل js/main.js بما يلي:
'use strict';

var isInitiator;

window.room = prompt("Enter room name:");

var socket = io.connect();

if (room !== "") {
  console.log('Message from client: Asking to join room ' + room);
  socket.emit('create or join', room);
}

socket.on('created', function(room, clientId) {
  isInitiator = true;
});

socket.on('full', function(room) {
  console.log('Message from client: Room ' + room + ' is full :^(');
});

socket.on('ipaddr', function(ipaddr) {
  console.log('Message from client: Server IP address is ' + ipaddr);
});

socket.on('joined', function(room, clientId) {
  isInitiator = false;
});

socket.on('log', function(array) {
  console.log.apply(console, array);
});

إعداد ملف Socket.IO لتشغيله على Node.js

في ملف HTML، ربما لاحظت أنك تستخدم ملف Socket.IO:

<script src="/socket.io/socket.io.js"></script>
  1. في المستوى الأعلى من دليل work، أنشِئ ملفًا باسم package.json يتضمَّن المحتوى التالي:
{
  "name": "webrtc-codelab",
  "version": "0.0.1",
  "description": "WebRTC codelab",
  "dependencies": {
    "node-static": "^0.7.10",
    "socket.io": "^1.2.0"
  }
}

هذا بيان تطبيق يوضّح "مدير حزمة العقدة" (npm) المشروع

تبعيات لتثبيتها.

  1. لتثبيت التبعيات، مثل /socket.io/socket.io.js، يمكنك تشغيل ما يلي من الوحدة الطرفية لسطر الأوامر في دليل work:
npm install

من المفترض أن يظهر لك سجلّ تثبيت ينتهي بـ:

3ab06b7bcc7664b9.png

يمكنك الاطّلاع على أنه تم تثبيت تبعيات تم تحديدها في package.json من قِبل npm.

  1. أنشِئ ملفًا جديدًا index.js في المستوى الأعلى من دليل work (وليس دليل js) وأضِف الرمز التالي:
'use strict';

var os = require('os');
var nodeStatic = require('node-static');
var http = require('http');
var socketIO = require('socket.io');

var fileServer = new(nodeStatic.Server)();
var app = http.createServer(function(req, res) {
  fileServer.serve(req, res);
}).listen(8080);

var io = socketIO.listen(app);
io.sockets.on('connection', function(socket) {

  // Convenience function to log server messages on the client
  function log() {
    var array = ['Message from server:'];
    array.push.apply(array, arguments);
    socket.emit('log', array);
  }

  socket.on('message', function(message) {
    log('Client said: ', message);
    // For a real app, would be room-only (not broadcast)
    socket.broadcast.emit('message', message);
  });

  socket.on('create or join', function(room) {
    log('Received request to create or join room ' + room);

    var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;

    log('Room ' + room + ' now has ' + numClients + ' client(s)');

    if (numClients === 0) {
      socket.join(room);
      log('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);

    } else if (numClients === 1) {
      log('Client ID ' + socket.id + ' joined room ' + room);
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room, socket.id);
      io.sockets.in(room).emit('ready');
    } else { // max two clients
      socket.emit('full', room);
    }
  });

  socket.on('ipaddr', function() {
    var ifaces = os.networkInterfaces();
    for (var dev in ifaces) {
      ifaces[dev].forEach(function(details) {
        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
          socket.emit('ipaddr', details.address);
        }
      });
    }
  });

});
  1. من الوحدة الطرفية لسطر الأوامر، شغِّل الأمر التالي في الدليل work:
node index.js
  1. الانتقال إلى http://localhost:8080 من المتصفّح

في كل مرة تنتقل فيها إلى عنوان URL هذا، يُطلب منك إدخال اسم غرفة.

للانضمام إلى الغرفة نفسها، أدخِل اسم الغرفة نفسه في كل مرة، مثل foo.

  1. افتح علامة تبويب جديدة وانتقِل إلى http://localhost:8080 وأدخِل اسم الغرفة نفسه مرة أخرى.
  2. افتح علامة تبويب جديدة أخرى، وانتقل إلى http://localhost:8080 مرة أخرى، وأدخل اسم الغرفة نفسه مرة أخرى.
  3. افحص وحدة التحكم في كل علامة تبويب.

من المفترض أن تظهر لك عملية تسجيل الدخول من JavaScript.

الحصول على نقاط إضافية

  • ما هي آليات المراسلة البديلة التي قد تكون ممكنة؟ ما المشاكل التي قد تواجهها عند استخدام تطبيق Pure WebSocket؟
  • ما المشاكل التي قد ينطوي عليها توسيع نطاق هذا التطبيق؟ هل يمكنك تطوير طريقة لاختبار الآلاف أو الملايين من طلبات الغرف المتزامنة؟
  • يستخدم هذا التطبيق رسالة مطالبة JavaScript للحصول على اسم غرفة. تعرّف على كيفية الحصول على اسم الغرفة من عنوان URL. على سبيل المثال، http://localhost:8080/foo سيمنح اسم الغرفة foo.

التعرف على المزيد

7- الجمع بين الاتصال بين الزملاء والإشارات

تتوفّر نسخة كاملة من هذه الخطوة في مجلد step-05.

استبدال HTML وJavaScript

  1. استبدل محتوى index.html بما يلي:
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <div id="videos">
    <video id="localVideo" autoplay muted></video>
    <video id="remoteVideo" autoplay></video>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>
  1. استبدل js/main.js بمحتوى step-05/js/main.js.

تشغيل خادم Node.js

إذا لم تكن تتابع هذا الدرس التطبيقي حول الترميز من دليل work، قد تحتاج إلى تثبيت تبعيات مجلد step-05 أو مجلد العمل الحالي.

  1. شغِّل الأمر التالي من دليل العمل:
npm install
  1. بعد التثبيت، في حال لم يكن خادم Node.js قيد التشغيل، يمكنك تشغيله عن طريق تشغيل الأمر التالي في الدليل work:
node index.js

تأكد من أنك تستخدم إصدار index.js من الخطوة السابقة التي تستخدم Socket.IO. لمزيد من المعلومات حول العقدة IO وSocket IO، راجع القسم "إعداد خدمة إشارة" لتبادل الرسائل.

  1. الانتقال إلى http://localhost:8080 من المتصفّح
  2. افتح علامة تبويب جديدة وانتقل إلى http://localhost:8080 مرة أخرى.

يعرض عنصر video البث المحلي من getUserMedia() ويعرض الآخر الفيديو البعيد عن طريق RTCPeerconnection.

  1. عرض التسجيل في وحدة تحكم المتصفح.

نقاط b****onus للنتائج

  • لا يدعم هذا التطبيق سوى محادثة الفيديو بين شخصين. كيف يمكنك تغيير التصميم للسماح لأكثر من مستخدم بمشاركة غرفة محادثة الفيديو نفسها؟
  • يحتوي المثال على اسم الغرفة foo برموز ثابتة. ما هي أفضل طريقة لتفعيل أسماء الغرف الأخرى؟
  • كيف سيشارك المستخدمون اسم الغرفة؟ حاوِل إنشاء بديل لمشاركة أسماء الغرف.
  • كيف يمكنك تغيير التطبيق؟

نصائح

  • يمكنك البحث عن إحصاءات WebRTC وبيانات تصحيح الأخطاء على chrome://webrtc-internals.
  • استخدِم أداة حل المشاكل WebRTC لاختبار البيئة المحلية واختبار الكاميرا والميكروفون.
  • في حال مواجهة مشاكل غريبة في التخزين المؤقت، يمكنك تجربة ما يلي:
  1. اضغط على Control وانقر على إعادة تحميل هذه الصفحة.
  2. أعِد تشغيل المتصفح.
  3. شغِّل npm cache clean من سطر الأوامر.

8- التقاط صورة ومشاركتها من خلال قناة بيانات

تتوفّر نسخة كاملة من هذه الخطوة في مجلد step-06.

آلية العمل

لقد تعلّمت في السابق كيفية تبادل الرسائل النصية باستخدام RTCDataChannel. تتيح لك هذه الخطوة مشاركة الملفات بالكامل. في هذا المثال، يتم التقاط الصور باستخدام getUserMedia().

في ما يلي الأجزاء الأساسية في هذه الخطوة:

  1. إنشاء قناة بيانات.

لا تضيف أي ساحات مشاركات وسائط إلى اتصال التطبيقات المشابهة في هذه الخطوة.

  1. التقاط فيديو مضمّن في كاميرا الويب باستخدام getUserMedia():
var video = document.getElementById('video');

function grabWebCamVideo() {
  console.log('Getting user media (video) ...');
  navigator.mediaDevices.getUserMedia({
    video: true
  })
  .then(gotStream)
  .catch(function(e) {
    alert('getUserMedia() error: ' + e.name);
  });
}
  1. انقر على محاذاة للحصول على لقطة (إطار فيديو) من الفيديو المضمّن وعرضها في عنصر canvas:
var photo = document.getElementById('photo');
var photoContext = photo.getContext('2d');

function snapPhoto() {
  photoContext.drawImage(video, 0, 0, photo.width, photo.height);
  show(photo, sendBtn);
}
  1. انقر على إرسال لتحويل الصورة إلى بايت وإرسالها عبر قناة بيانات:
function sendPhoto() {
  // Split the data-channel message in chunks of this byte length.
  var CHUNK_LEN = 64000;
  var img = photoContext.getImageData(0, 0, photoContextW, photoContextH),
    len = img.data.byteLength,
    n = len / CHUNK_LEN | 0;

  console.log('Sending a total of ' + len + ' byte(s)');
  dataChannel.send(len);

  // Split the photo and send in chunks of approximately 64KB.
  for (var i = 0; i < n; i++) {
    var start = i * CHUNK_LEN,
      end = (i + 1) * CHUNK_LEN;
    console.log(start + ' - ' + (end - 1));
    dataChannel.send(img.data.subarray(start, end));
  }

  // Send the reminder, if applicable.
  if (len % CHUNK_LEN) {
    console.log('last ' + len % CHUNK_LEN + ' byte(s)');
    dataChannel.send(img.data.subarray(n * CHUNK_LEN));
  }
}

يحوِّل جانب الاستلام وحدات بايت رسالة قناة البيانات إلى صورة ويعرض الصورة للمستخدم:

function receiveDataChromeFactory() {
  var buf, count;

  return function onmessage(event) {
    if (typeof event.data === 'string') {
      buf = window.buf = new Uint8ClampedArray(parseInt(event.data));
      count = 0;
      console.log('Expecting a total of ' + buf.byteLength + ' bytes');
      return;
    }

    var data = new Uint8ClampedArray(event.data);
    buf.set(data, count);

    count += data.byteLength;
    console.log('count: ' + count);

    if (count === buf.byteLength) {
      // we're done: all data chunks have been received
      console.log('Done. Rendering photo.');
      renderPhoto(buf);
    }
  };
}

function renderPhoto(data) {
  var canvas = document.createElement('canvas');
  canvas.width = photoContextW;
  canvas.height = photoContextH;
  canvas.classList.add('incomingPhoto');
  // The trail is the element that holds the incoming images.
  trail.insertBefore(canvas, trail.firstChild);

  var context = canvas.getContext('2d');
  var img = context.createImageData(photoContextW, photoContextH);
  img.data.set(data);
  context.putImageData(img, 0, 0);
}

الحصول على الرمز‏

  1. استبدل محتوى مجلد work بمحتوى step-06.

من المفترض أن يظهر ملف index.html في work على النحو التالي الآن**:**

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <h2>
    <span>Room URL: </span><span id="url">...</span>
  </h2>

  <div id="videoCanvas">
    <video id="camera" autoplay></video>
    <canvas id="photo"></canvas>
  </div>

  <div id="buttons">
    <button id="snap">Snap</button><span> then </span><button id="send">Send</button>
    <span> or </span>
    <button id="snapAndSend">Snap &amp; Send</button>
  </div>

  <div id="incoming">
    <h2>Incoming photos</h2>
    <div id="trail"></div>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>
  1. إذا لم تكن تتابع هذا الدرس التطبيقي حول الترميز من دليل work، قد تحتاج إلى تثبيت تبعيات مجلد step-06 أو مجلد العمل الحالي. ما عليك سوى تشغيل الأمر التالي من دليل العمل:
npm install
  1. بعد التثبيت، في حال لم يكن خادم Node.js قيد التشغيل، يمكنك تشغيله عن طريق تشغيل الأمر التالي من دليل work:
node index.js
    Make sure that you're using the version of `index.js` that implements Socket.IO and 

تذكّر إعادة تشغيل خادم Node.js إذا أجريت تغييرات.

لمزيد من المعلومات حول العقدة وSocket.IO، راجع القسم "إعداد إشارات".

الخدمة لتبادل الرسائل.

  1. إذا لزم الأمر، انقر على سماح للسماح للتطبيق باستخدام كاميرا الويب.

ينشئ التطبيق رقم تعريف غرفة عشوائيًا ويضيف رقم التعريف إلى عنوان URL.

  1. افتح عنوان URL من شريط العناوين في علامة تبويب أو نافذة متصفح جديدة.
  2. انقر على Snap & Send ثم اطّلع على الصور الواردة في علامة التبويب الأخرى في أسفل الصفحة.

ينقل التطبيق الصور بين علامات التبويب.

ينبغي أن تظهر لك على النحو التالي:

911b40f36ba6ba8.png

الحصول على نقاط إضافية

كيف يمكنك تغيير الرمز لتمكين مشاركة أي نوع من الملفات؟

التعرف على المزيد

9- تهانينا

لقد أنشأت تطبيقًا لبث الفيديو وتبادل البيانات في الوقت الفعلي.

مزيد من المعلومات