Это модель видео-связи без сигнального сервера. Обмен установочной информацией SDP и Ice Candidates производится вручную. Для приема-передачи SDP и ICE Candidates можно использовать e-mail или 🙂 голубиную почту. В действительности, приемом/передачей установочной служебной информацией (SDP, Ice candidates) занимается сигнальный сервер. Но это – отдельная тема разговора.
Сценарий организации двухсторонней видеосвязи в ручную:
- Первый участник: нажимает кнопку Offer.
- Первый участник: копирует содержимое окна Offer и отсылает его по email Второму участнику.
- Второй участник: копирует содержимое полученного email в окно Offer и нажимает кнопку Enter на клавиатуре.
- Второй участник: копирует содержимое окна Answer и отсылает его по email Первому участнику.
- Первый участник: копирует содержимое полученного 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(); }