Это модель видео-связи без сигнального сервера. Обмен установочной информацией 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();
}
