WebRTC для двоих

Это модель видео-связи без сигнального сервера. Обмен установочной информацией SDP и Ice Candidates производится вручную. Для приема-передачи SDP и ICE Candidates можно использовать e-mail или 🙂 голубиную почту. В действительности, приемом/передачей установочной служебной информацией (SDP, Ice candidates) занимается сигнальный сервер. Но это – отдельная тема разговора.

Посмотреть в действии

Сценарий организации двухсторонней видеосвязи в ручную:

  1. Первый участник: нажимает кнопку Offer.
  2. Первый участник: копирует содержимое окна Offer и отсылает его по email Второму участнику.
  3. Второй участник: копирует содержимое полученного email в окно Offer и нажимает кнопку Enter на клавиатуре.
  4. Второй участник: копирует содержимое окна Answer и отсылает его по email Первому участнику.
  5. Первый участник: копирует содержимое полученного email в окно Answer и нажимает кнопку Enter на клавиатуре.

p.s.: SDP следует высылать почтой в виде прикрепленного файла txt, созданного простым текстовым редактором Notepad.
В окне Chat появляются сообщения о текущем состоянии установления связи.
Окно Input предназначено для набора текстового сообщения другому участнику видеосвязи и его отправки после нажатия кнопки Enter на клавиатуре.
Все текстовые сообщения участников видеосвязи отображаются в окне Chat.
Ниже приведен код:

<html>
  <meta charset="UTF-8">
  <head>
    <title>example2</title>
    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css">
    <style>
      body {
       padding-top:5rem;
      }
    </style>
  </head>
  <body>
    <nav class="navbar navbar-fixed-top navbar-dark bg-inverse">
      <a class="navbar-brand" href="#">WebRTC without signaling server. The exchange of startup data is done manually.</a>
    </nav>
    <div class="container">
      <div class="row">
        <div class="col-sm-6">
          <h2>peer1</h2>
          <video controls style="border: 4px double black;" id="video1" width="100%" height="400px" muted autoplay ></video>
          <p>
            <label>Offer: </label><textarea id="fld_offer" class="form-control" placeholder="Paste offer here"></textarea>
          </p>
          <p>
            <button id="button" onclick="createOffer()">Offer:</button>
          </p>
          <label>Chat: </label><div id="fld_log" class="form-control">List of messages...</div>
        </div>

        <div class="col-sm-6">
          <h2>peer2</h2>
          <video controls style="border: 4px double black;" id="video2" width="100%" height="400px" autoplay></video>
          <p>
            <label>Answer: </label><textarea id="fld_answer" class="form-control"></textarea>
          </p>
          <p>
            <div style="height: 30px;"></div>
          </p>
          <label>Input: </label><input id="fld_chat" class="form-control"></input><br>
        </div>
      </div>
    </div>
  </body>
</html>
var server = { urls: "stun:stun.l.google.com:19302" };
var dc, pc = new RTCPeerConnection({ iceServers: [server] });
var mediaStream, fld_offer, fld_answer;
var enterPressed = e => e.keyCode == 13;
var log = msg => fld_log.innerHTML += "<p>" + msg + "</p>";

window.onload = function() {
  var video1 = document.getElementById('video1');
  var video2 = document.getElementById('video2');
  fld_offer  = document.getElementById('fld_offer');
  fld_answer = document.getElementById('fld_answer');

  pc.ondatachannel = e => dcInit(dc = e.channel);
  pc.oniceconnectionstatechange = e => log(pc.iceConnectionState);

  mediaStream = navigator.mediaDevices.getUserMedia({video:true, audio:true})
  .then(stream => {
    video1.srcObject = stream;
    stream.getTracks().forEach(track => pc.addTrack(track, stream));
  })
  .catch(function (error) {
    log;
    var stream_err = wm_canvas(error);
    video1.srcObject = stream_err;
    stream_err.getTracks().forEach(track => pc.addTrack(track, wm_canvas(error)));
  })
  .then(() => pc.ontrack = e => (video2.srcObject = e.streams[0]));

  fld_offer.onkeypress = e => {
    if (!enterPressed(e) || pc.signalingState != "stable") return;
    button.disabled = fld_offer.disabled = true;
    var desc = new RTCSessionDescription({ type:"offer", sdp:fld_offer.value });
    pc.setRemoteDescription(desc)
      .then(() => pc.createAnswer()).then(d => pc.setLocalDescription(d))
      .catch(log);
    pc.onicecandidate = e => {
      if (e.candidate) return;
      fld_answer.focus();
      fld_answer.value = pc.localDescription.sdp;
      fld_answer.select();
    };
  };

  fld_answer.onkeypress = e => {
    if (!enterPressed(e) || pc.signalingState != "have-local-offer") return;
    fld_answer.disabled = true;
    var desc = new RTCSessionDescription({ type:"answer", sdp:fld_answer.value });
    pc.setRemoteDescription(desc).catch(log);
  };

  fld_chat.onkeypress = e => {
    if (!enterPressed(e)) return;
    dc.send(fld_chat.value);
    log(fld_chat.value);
    fld_chat.value = "";
  };
}

function dcInit() {
  dc.onopen = () => log("Chat!");
  dc.onmessage = e => log(e.data);
}

function createOffer() {
  button.disabled = true;
  dcInit(dc = pc.createDataChannel("chat"));
  mediaStream.then(() => pc.createOffer()).then(d => pc.setLocalDescription(d))
    .catch(log);
  pc.onicecandidate = e => {
    if (e.candidate) return;
    fld_offer.value = pc.localDescription.sdp;
    fld_offer.select();
    fld_answer.placeholder = "Paste answer here";
  };
};
/**
 * Play video of getUserMedia from canvas.
 *
 * @param object $error Error of getUserMedia.
 */
function wm_canvas(error) {
  let style_div = getComputedStyle(document.getElementById('video1'));
  let canvas    = document.createElement('canvas');

  let whiteNoise = () => {
    let ctx = canvas.getContext('2d');
    ctx.fillRect(0, 0, parseInt(style_div.width), parseInt(style_div.height));
    let p = ctx.getImageData(0, 0, parseInt(style_div.width), parseInt(style_div.height));
    requestAnimationFrame(function draw(){
      for (var i = 0; i < p.data.length; i++) {
        p.data[i++] = p.data[i++] = p.data[i++] = Math.random() * 255;
      }
      ctx.putImageData(p, 0, 0);
      requestAnimationFrame(draw);
    });
    return canvas;
  }
  return whiteNoise().captureStream();
}