Статьи

Создание вашего первого приложения WebRTC

Вступление

Интернет-коммуникация всегда была неотъемлемой частью как крупных, так и средних предприятий. Полнофункциональное и хорошо оснащенное интернет-общение — это общение посредством видео, текста, аудио или электронной почты на единой платформе. Все каналы связи требуют разных технологий. Видео и аудио связь часто требует специальных плагинов. Благодаря последним технологическим инновациям в форме API WebRTC сценарий полностью изменился.

Давайте поговорим о том, как мы создаем наше первое приложение HTML WebRTC, которое может поддерживать как аудио, так и видео связь.

Прежде чем начать, позвольте мне сказать вам, что WebRTC является API веб-браузера и, следовательно, чтобы он работал, ваш браузер должен поддерживать WebRTC. И все поклонники WebRTC будут рады узнать, что браузеры Mozilla Firefox и Google Chrome поддерживают WebRTC.

Распространение слухов

Некоторые прикладные платформы утверждают, что им разрешен «WebRTC», но в действительности они поддерживают только getUserMedia , а getUserMedia не поддерживает остальные компоненты RTC.

Следовательно, перед разработкой тщательно выполните жизненно важный сеанс проверки платформы.

Технические особенности WebRTC

Давайте внимательно посмотрим, каковы технические ожидания от приложений WebRTC :

  1. Потоковое аудио, видео или других данных.

  2. Получение сетевой информации, такой как IP-адреса и порты. Обмен этой информацией с другими клиентами (одноранговыми узлами) WebRTC для подключения даже через NAT и брандмауэры.

  3. В случае сообщения об ошибках, инициации, закрытия сеанса и координации с передачей сигналов.

  4. Общение с потоковым видео, аудио или данными.

  5. Обмен информацией о медиа и клиентских возможностях, таких как разрешение и кодеки.

Вышеуказанные функции были реализованы WebRTC с использованием некоторых основных API, перечисленных ниже:

  1. MediaStream (он же getUserMedia)

  2. RTCPeerConnection

  3. RTCDataChannel

Давайте внимательно посмотрим на эти API

1. MediaStream : getUserMedia или MediaStream получает доступ к потокам данных, например, с камеры пользователя и микрофона. MediaStream доступен в Chrome, Firefox и Opera. MediaSream API представляет синхронизированные потоки мультимедиа. Это может быть хорошо объяснено, что поток, взятый с входа камеры и микрофона, имеет синхронизированные видео и аудио дорожки. Каждый MediaStream имеет вход, который может быть MediaStrem, сгенерированным навигатором. getUserMedia () , и выходные данные могли быть переданы элементу видео или соединению RTCPeer.

Вы должны знать, что метод getUserMedia () принимает три параметра:

    1. Объект ограничения.

    2. Успешный обратный вызов, при вызове которого передается MediaStream.

    3. Сбой обратного вызова, при вызове которого передается объект ошибки.

Каждый MediaStream имеет метку, например «Xk7EuLhsuHKbnjLWkW4yYGNJJ8ONsgwHBvLQ». Массив MediaStreamTracks возвращается методами getAudioTracks () и getVideoTracks () .

Для примера simp.info/gum stream.getAudioTracks () возвращает пустой массив (потому что нет звука), и, если подключена рабочая веб-камера, stream.getVideoTracks () возвращает массив из одного MediaStreamTrack, представляющего поток с веб-камеры , Каждый MediaStreamTrack имеет вид («видео» или «аудио») и метку (например, «HD-камера FaceTime (встроенная)») и представляет один или несколько каналов аудио или видео. В этом случае есть только одна видеодорожка и нет звука, но вы можете легко представить себе случаи использования, где их больше: например, приложение для чата, которое получает потоки с передней камеры, задней камеры, микрофона и общего экрана. ‘ применение.

Обратите внимание: getUserMedia также может использоваться в качестве узла ввода для API Web Audio:

function gotStream(stream) {
    window.AudioContext = window.AudioContext || window.webkitAudioContext;
    var audioContext = new AudioContext();

    // Create an AudioNode from the stream
    var mediaStreamSource = audioContext.createMediaStreamSource(stream);

    // Connect it to destination to hear yourself
    // or any other node for processing!
    mediaStreamSource.connect(audioContext.destination);
}

navigator.getUserMedia({audio:true}, gotStream);

getUserMedia также может быть добавлен в приложения и расширения на основе Chromium. Добавление разрешений audioCapture и / или videoCapture позволяет запрашивать разрешение и предоставлять его только один раз при установке. После этого пользовательское разрешение на доступ к камере или микрофону не запрашивается.

Аналогично, страницы, использующие HTTPS: разрешение должно быть предоставлено только один раз для getUserMedia (). Впервые кнопка «Всегда разрешать» отображается в информационной панели браузера.

Это всегда требуется при включении MediaStream для любого источника потоковых данных, а не только для камеры или микрофона. Это позволяет осуществлять потоковую передачу с диска или из произвольных источников данных, таких как другие входы и датчики.

Обратите внимание, что getUserMedia () должен использоваться на сервере, а не в локальной файловой системе, в противном случае будет выдано сообщение об ошибке PERMISSION_DENIED: 1.

2. RTCPeerConnection : аудио- или видеовызовы содержат расширение управления шифрованием и полосой пропускания. Он поддерживается в Chrome (как для настольных ПК, так и для Android), Opera (для настольных ПК и Android) и, конечно, в Firefox. RTCPeerConnection реализуется Chrome и Opera как webkitRTCPeerConnection и Firefox как mozRTCPeerConnection. Сверхпростая демонстрация реализации RTCPeerConnection в Chromium по адресу simp.info/pc и отличное приложение для видеочата на apprtc.appspot.com. Это приложение использует adap.js, JavaScript-шим, поддерживаемый Google, который абстрагирует различия между браузерами и изменениями спецификаций.

RTCPeerConnection — это компонент WebRTC,  который обеспечивает стабильную и эффективную передачу потоковых данных между узлами.

Давайте внимательно рассмотрим диаграмму архитектуры WebRTC, которая демонстрирует роль RTCPeerConnection.

Рассматривая диаграмму с точки зрения JavaScript, вы должны понимать, что RTCPeerConnection защищает веб-разработчиков от множества сложностей. Кодеки и протоколы, используемые WebRTC, позаботятся о огромном объеме работы для обеспечения связи в реальном времени даже по ненадежным сетям:

  • Сокрытие потери пакета

  • Эхоподавление

  • Адаптивность полосы пропускания

  • Динамическая буферизация джиттера

  • Автоматическая регулировка усиления

  • Шумоподавление и подавление

  • Изображение ‘чистка.’

Давайте обсудим чрезмерную сигнализацию:

2 (a) Сигнализация: управление сеансом, информация о сети и медиа

WebRTC использует RTCPeerConnection для передачи потоковых данных между браузерами. Наряду с этим ему также необходим механизм для координации связи и отправки управляющих сообщений. Этот процесс может быть определен как сигнализация. Следует знать, что сигнализация не является частью API RTCPeerConnection. Разработчики приложений WebRTC могут выбрать любой протокол обмена сообщениями, который они предпочитают, например, SIP или XMPP, а также соответствующий дуплексный (двусторонний) канал связи.

В итоге пользователи узнают друг друга и обмениваются такими деталями, как имена, в реальном мире. Клиентские приложения WebRTC обмениваются сетевой информацией. Пэры обмениваются данными о медиа, таких как формат видео и разрешение. Клиентские приложения WebRTC пересекают NAT-шлюзы и межсетевые экраны.

Из этого также можно понять, что WebRTC требуется четыре типа серверной функциональности:

    • Обнаружение пользователей и общение.

    • Передача сигналов.

    • NAT / межсетевой экран.

    •  Серверы ретрансляции в случае сбоя одноранговой связи.

3. RTCDataChannel : одноранговая передача общих данных. API поддерживается в Chrome 25, Opera 18 и Firefox 22 и выше.

Пусть кодирование НАЧИНАЕТСЯ!

Предполагая, что у вас достаточно хорошие технические навыки и навыки программирования JavaScript, HTML и CSS, а также Node.JS.

Давайте начнем с кодирования.

Шаг 1. Создайте пустой документ HTML5

Создайте чистый HTML-документ.

<script type="text/javascript" src="js/lib/adapter.js"></script>
  <script type="text/javascript">// <![CDATA[

// ]]>
   </script>

Шаг 2: Получить видео с вашей веб-камеры

  1. Добавьте элемент видео на свою страницу.

  2. Добавьте следующий JavaScript-код к элементу script на вашей странице, чтобы getUserMedia () установила источник видео с веб-камеры:

  3. Проверьте это локально

var constraints = {video: true};

function successCallback(localMediaStream) {
  window.stream = localMediaStream; // stream available to console
  var video = document.querySelector("video");
  video.src = window.URL.createObjectURL(localMediaStream);
  video.play();
}

function errorCallback(error){
  console.log("navigator.getUserMedia error: ", error);
}

navigator.getUserMedia(constraints, successCallback, errorCallback);

Внимательно посмотрите, что именно мы здесь сделали. Сначала мы вызвали getUserMedia, используя:

navigator.getUserMedia(constraints, successCallback, errorCallback);

Аргумент constraints позволяет нам указать медиа для получения, только в этом случае видео:

var constraints = {"video": true}

В случае успеха видеопоток с веб-камеры устанавливается как источник элемента видео:

function successCallback(localMediaStream) {
  window.stream = localMediaStream; // stream available to console
  var video = document.querySelector("video");
  video.src = window.URL.createObjectURL(localMediaStream);
  video.play();
}

Шаг 3: настроить сервер сигнализации и начать обмен сообщениями

В реальных приложениях нет отправителя и получателя RTCPeerConnections на одной странице, поэтому нам нужен способ для передачи метаданных.

Для его применения нам потребуется сервер сигнализации: сервер, который может обмениваться сообщениями между приложением (клиентом) WebRTC, работающим в одном браузере, и клиентом в другом браузере. Фактические сообщения — это строковые объекты JavaScript.

На этом этапе мы создадим простой сервер сигнализации Node.js, используя модуль Node socket.io и библиотеку JavaScript для обмена сообщениями. Опыт работы с Node.js и socket.io будет полезен, но не критичен — компоненты обмена сообщениями очень просты. В этом примере сервер (приложение Node) — это server.js, а клиент (веб-приложение) — index.html.

Приложение Node-сервера на этом шаге имеет две задачи.

    1. Выступать посредником в обмене сообщениями:

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

     2. и управлять «комнатами» видеочата WebRTC:

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

Наше простое приложение WebRTC разрешит разделять комнату максимум двум пирам. Убедитесь, что у вас установлены Node, socket.io и node-static. Чтобы установить socket.io и node-static, запустите Node Package Manager из терминала в каталоге вашего приложения:

npm install socket.io
npm install node-static

Теперь у вас есть три отдельных файла: index.html, server.js и ваш основной файл JavaScript main.js. Они бы выглядели примерно так.

1. main.js

var static = require('node-static');
var http = require('http');
var file = new(static.Server)();
var app = http.createServer(function (req, res) {
  file.serve(req, res);
}).listen(2013);

var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket){

  // convenience function to log server messages on the client
function log(){
var array = [">>> Message from server: "];
  for (var i = 0; i < arguments.length; i++) {
  array.push(arguments[i]);
  }
    socket.emit('log', array);
}

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

socket.on('create or join', function (room) {
var numClients = io.sockets.clients(room).length;

log('Room ' + room + ' has ' + numClients + ' client(s)');
log('Request to create or join room ' + room);

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

});

});

2. index.html

WebRTC client

<script type="text/javascript" src="http://localhost:2013/socket.io/socket.io.js"></script>
<script type="text/javascript" src="js/lib/adapter.js"></script>
<script type="text/javascript" src="js/main.js"></script>

3. server.js

var isInitiator;

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

var socket = io.connect();

if (room !== "") {
  console.log('Joining room ' + room);
  socket.emit('create or join', room);
}

socket.on('full', function (room){
  console.log('Room ' + room + ' is full');
});

socket.on('empty', function (room){
  isInitiator = true;
  console.log('Room ' + room + ' is empty');
});

socket.on('join', function (room){
  console.log('Making request to join room ' + room);
  console.log('You are the initiator!');
});

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

Чтобы запустить сервер, выполните следующую команду из терминала в каталоге вашего приложения:

node server.js

Шаг 4: Подключение всего

Теперь мы собираемся все соединить. Мы собираемся добавить сигнализацию нашему видео клиенту, созданному на шаге 2. Мы также собираемся внедрить RTCPeerConnection и RTCDataChannel в наше приложение.

1. Добавьте следующий код в ваш основной HTML-файл:

</pre>
<div id="container">
<div id="videos">
</div>
<div id="textareas"><textarea id="dataChannelSend" disabled="disabled"></textarea>
 <textarea id="dataChannelReceive" disabled="disabled"></textarea></div>
 <button id="sendButton" disabled="disabled">Send</button></div>
<pre>

2. Ваш основной файл JavaScript, main.js, будет выглядеть примерно так:

'use strict';

var sendChannel;
var sendButton = document.getElementById("sendButton");
var sendTextarea = document.getElementById("dataChannelSend");
var receiveTextarea = document.getElementById("dataChannelReceive");

sendButton.onclick = sendData;

var isChannelReady;
var isInitiator;
var isStarted;
var localStream;
var pc;
var remoteStream;
var turnReady;

var pc_config = webrtcDetectedBrowser === 'firefox' ?
  {'iceServers':[{'url':'stun:23.21.150.121'}]} : // number IP
  {'iceServers': [{'url': 'stun:stun.l.google.com:19302'}]};

var pc_constraints = {
  'optional': [
    {'DtlsSrtpKeyAgreement': true},
    {'RtpDataChannels': true}
  ]};

// Set up audio and video regardless of what devices are present.
var sdpConstraints = {'mandatory': {
  'OfferToReceiveAudio':true,
  'OfferToReceiveVideo':true }};

/////////////////////////////////////////////

var room = location.pathname.substring(1);
if (room === '') {
//  room = prompt('Enter room name:');
  room = 'foo';
} else {
  //
}

var socket = io.connect();

if (room !== '') {
  console.log('Create or join room', room);
  socket.emit('create or join', room);
}

socket.on('created', function (room){
  console.log('Created room ' + room);
  isInitiator = true;
});

socket.on('full', function (room){
  console.log('Room ' + room + ' is full');
});

socket.on('join', function (room){
  console.log('Another peer made a request to join room ' + room);
  console.log('This peer is the initiator of room ' + room + '!');
  isChannelReady = true;
});

socket.on('joined', function (room){
  console.log('This peer has joined room ' + room);
  isChannelReady = true;
});

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

////////////////////////////////////////////////

function sendMessage(message){
console.log('Sending message: ', message);
  socket.emit('message', message);
}

socket.on('message', function (message){
  console.log('Received message:', message);
  if (message === 'got user media') {
  maybeStart();
  } else if (message.type === 'offer') {
    if (!isInitiator && !isStarted) {
      maybeStart();
    }
    pc.setRemoteDescription(new RTCSessionDescription(message));
    doAnswer();
  } else if (message.type === 'answer' && isStarted) {
    pc.setRemoteDescription(new RTCSessionDescription(message));
  } else if (message.type === 'candidate' && isStarted) {
    var candidate = new RTCIceCandidate({sdpMLineIndex:message.label,
      candidate:message.candidate});
    pc.addIceCandidate(candidate);
  } else if (message === 'bye' && isStarted) {
    handleRemoteHangup();
  }
});

////////////////////////////////////////////////////

var localVideo = document.querySelector('#localVideo');
var remoteVideo = document.querySelector('#remoteVideo');

function handleUserMedia(stream) {
  localStream = stream;
  attachMediaStream(localVideo, stream);
  console.log('Adding local stream.');
  sendMessage('got user media');
  if (isInitiator) {
    maybeStart();
  }
}

function handleUserMediaError(error){
  console.log('getUserMedia error: ', error);
}

var constraints = {video: true};

getUserMedia(constraints, handleUserMedia, handleUserMediaError);
console.log('Getting user media with constraints', constraints);

if (location.hostname != "localhost") {
  requestTurn('https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913');
}

function maybeStart() {
  if (!isStarted && localStream && isChannelReady) {
    createPeerConnection();
    pc.addStream(localStream);
    isStarted = true;
    if (isInitiator) {
      doCall();
    }
  }
}

window.onbeforeunload = function(e){
sendMessage('bye');
}

/////////////////////////////////////////////////////////

function createPeerConnection() {
  try {
    pc = new RTCPeerConnection(pc_config, pc_constraints);
    pc.onicecandidate = handleIceCandidate;
    console.log('Created RTCPeerConnnection with:n' +
      '  config: '' + JSON.stringify(pc_config) + '';n' +
      '  constraints: '' + JSON.stringify(pc_constraints) + ''.');
  } catch (e) {
    console.log('Failed to create PeerConnection, exception: ' + e.message);
    alert('Cannot create RTCPeerConnection object.');
      return;
  }
  pc.onaddstream = handleRemoteStreamAdded;
  pc.onremovestream = handleRemoteStreamRemoved;

  if (isInitiator) {
    try {
      // Reliable Data Channels not yet supported in Chrome
      sendChannel = pc.createDataChannel("sendDataChannel",
        {reliable: false});
      sendChannel.onmessage = handleMessage;
      trace('Created send data channel');
    } catch (e) {
      alert('Failed to create data channel. ' +
            'You need Chrome M25 or later with RtpDataChannel enabled');
      trace('createDataChannel() failed with exception: ' + e.message);
    }
    sendChannel.onopen = handleSendChannelStateChange;
    sendChannel.onclose = handleSendChannelStateChange;
  } else {
    pc.ondatachannel = gotReceiveChannel;
  }
}

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

function gotReceiveChannel(event) {
  trace('Receive Channel Callback');
  sendChannel = event.channel;
  sendChannel.onmessage = handleMessage;
  sendChannel.onopen = handleReceiveChannelStateChange;
  sendChannel.onclose = handleReceiveChannelStateChange;
}

function handleMessage(event) {
  trace('Received message: ' + event.data);
  receiveTextarea.value = event.data;
}

function handleSendChannelStateChange() {
  var readyState = sendChannel.readyState;
  trace('Send channel state is: ' + readyState);
  enableMessageInterface(readyState == "open");
}

function handleReceiveChannelStateChange() {
  var readyState = sendChannel.readyState;
  trace('Receive channel state is: ' + readyState);
  enableMessageInterface(readyState == "open");
}

function enableMessageInterface(shouldEnable) {
    if (shouldEnable) {
    dataChannelSend.disabled = false;
    dataChannelSend.focus();
    dataChannelSend.placeholder = "";
    sendButton.disabled = false;
  } else {
    dataChannelSend.disabled = true;
    sendButton.disabled = true;
  }
}

function handleIceCandidate(event) {
  console.log('handleIceCandidate event: ', event);
  if (event.candidate) {
    sendMessage({
      type: 'candidate',
      label: event.candidate.sdpMLineIndex,
      id: event.candidate.sdpMid,
      candidate: event.candidate.candidate});
  } else {
    console.log('End of candidates.');
  }
}

function doCall() {
  var constraints = {'optional': [], 'mandatory': {'MozDontOfferDataChannel': true}};
  // temporary measure to remove Moz* constraints in Chrome
  if (webrtcDetectedBrowser === 'chrome') {
    for (var prop in constraints.mandatory) {
      if (prop.indexOf('Moz') !== -1) {
        delete constraints.mandatory[prop];
      }
     }
   }
  constraints = mergeConstraints(constraints, sdpConstraints);
  console.log('Sending offer to peer, with constraints: n' +
    '  '' + JSON.stringify(constraints) + ''.');
  pc.createOffer(setLocalAndSendMessage, null, constraints);
}

function doAnswer() {
  console.log('Sending answer to peer.');
  pc.createAnswer(setLocalAndSendMessage, null, sdpConstraints);
}

function mergeConstraints(cons1, cons2) {
  var merged = cons1;
  for (var name in cons2.mandatory) {
    merged.mandatory[name] = cons2.mandatory[name];
  }
  merged.optional.concat(cons2.optional);
  return merged;
}

function setLocalAndSendMessage(sessionDescription) {
  // Set Opus as the preferred codec in SDP if Opus is present.
  sessionDescription.sdp = preferOpus(sessionDescription.sdp);
  pc.setLocalDescription(sessionDescription);
  sendMessage(sessionDescription);
}

function requestTurn(turn_url) {
  var turnExists = false;
  for (var i in pc_config.iceServers) {
    if (pc_config.iceServers[i].url.substr(0, 5) === 'turn:') {
      turnExists = true;
      turnReady = true;
      break;
    }
  }
  if (!turnExists) {
    console.log('Getting TURN server from ', turn_url);
    // No TURN server. Get one from computeengineondemand.appspot.com:
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
      if (xhr.readyState === 4 && xhr.status === 200) {
        var turnServer = JSON.parse(xhr.responseText);
      console.log('Got TURN server: ', turnServer);
        pc_config.iceServers.push({
          'url': 'turn:' + turnServer.username + '@' + turnServer.turn,
          'credential': turnServer.password
        });
        turnReady = true;
      }
    };
    xhr.open('GET', turn_url, true);
    xhr.send();
  }
}

function handleRemoteStreamAdded(event) {
  console.log('Remote stream added.');
 // reattachMediaStream(miniVideo, localVideo);
  attachMediaStream(remoteVideo, event.stream);
  remoteStream = event.stream;
//  waitForRemoteVideo();
}
function handleRemoteStreamRemoved(event) {
  console.log('Remote stream removed. Event: ', event);
}

function hangup() {
  console.log('Hanging up.');
  stop();
  sendMessage('bye');
}

function handleRemoteHangup() {
  console.log('Session terminated.');
  stop();
  isInitiator = false;
}

function stop() {
  isStarted = false;
  // isAudioMuted = false;
  // isVideoMuted = false;
  pc.close();
  pc = null;
}

///////////////////////////////////////////

// Set Opus as the default audio codec if it's present.
function preferOpus(sdp) {
  var sdpLines = sdp.split('rn');
  var mLineIndex;
  // Search for m line.
  for (var i = 0; i < sdpLines.length; i++) {
      if (sdpLines[i].search('m=audio') !== -1) {
        mLineIndex = i;
        break;
      }
  }
  if (mLineIndex === null) {
    return sdp;
  }

  // If Opus is available, set it as the default in m line.
  for (i = 0; i < sdpLines.length; i++) {
    if (sdpLines[i].search('opus/48000') !== -1) {
      var opusPayload = extractSdp(sdpLines[i], /:(d+) opus/48000/i);
      if (opusPayload) {
        sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], opusPayload);
      }
      break;
    }
  }

  // Remove CN in m line and sdp.
  sdpLines = removeCN(sdpLines, mLineIndex);

  sdp = sdpLines.join('rn');
  return sdp;
}

function extractSdp(sdpLine, pattern) {
  var result = sdpLine.match(pattern);
  return result && result.length === 2 ? result[1] : null;
}

// Set the selected codec to the first in m line.
function setDefaultCodec(mLine, payload) {
  var elements = mLine.split(' ');
  var newLine = [];
  var index = 0;
  for (var i = 0; i < elements.length; i++) 
  {
    if (index === 3)
    {
      // Format of media starts from the fourth. newLine[index++] = payload;
      // Put target payload to the first.
    }     if (elements[i] !== payload)
    {       newLine[index++] = elements[i]; 
    }   }   return newLine.join(' ');
} 
// Strip CN from sdp before CN constraints is ready.
function removeCN(sdpLines, mLineIndex)
{   var mLineElements = sdpLines[mLineIndex].split(' '); 
 // Scan from end for the convenience of removing an item. 
 for (var i = sdpLines.length-1; i >= 0; i--) {
    var payload = extractSdp(sdpLines[i], /a=rtpmap:(d+) CN/d+/i);
    if (payload) {
      var cnPos = mLineElements.indexOf(payload);
      if (cnPos !== -1) {
        // Remove CN payload from m line.
        mLineElements.splice(cnPos, 1);
      }
      // Remove CN line in sdp
      sdpLines.splice(i, 1);
    }
  }

  sdpLines[mLineIndex] = mLineElements.join(' ');
  return sdpLines;
}

Вы закончили с первым приложением WebRTC

Браво!! Ты сделал это. Теперь вы являетесь счастливым обладателем нового приложения для видеочата WebRTC. Это может показаться вам довольно простым, но в нем есть вся суть твердой базы приложений. Теперь вы можете начать добавлять CSS, чтобы сделать страницу более привлекательной. В настоящее время ваше приложение учитывает только видеочат, поэтому, чтобы добавить больше, вы можете выбрать опцию «Добавить конференцию», и если вы чувствуете, что можете запустить множество мобильных приложений, как мы. 🙂