rtc

minimal webrtc client
git clone https://git.ce9e.org/rtc.git

commit
df5b534ec16e4b909191bbe0152f0d0c713715eb
parent
c70cf4759cc9c03e36362c13844dfe312f2f060d
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2020-04-03 19:23
use js modules

Diffstat

M www/index.html 3 +--
M www/rtc.js 418 ++++++++++++++++++++++++++++++------------------------------
M www/signal.js 53 ++++++++++++++++++++++-------------------------------

3 files changed, 232 insertions, 242 deletions


diff --git a/www/index.html b/www/index.html

@@ -36,7 +36,6 @@
   36    36 			<button>Send</button>
   37    37 		</form>
   38    38 	</div>
   39    -1 	<script src="signal.js"></script>
   40    -1 	<script src="rtc.js"></script>
   -1    39 	<script type="module" src="rtc.js"></script>
   41    40 </body>
   42    41 </html>

diff --git a/www/rtc.js b/www/rtc.js

@@ -1,244 +1,244 @@
    1    -1 /* global signal */
    2     1 /* eslint no-console: "off" */
    3    -1 (function() {
    4    -1 	// https://webrtc.github.io/samples/
    5    -1 	// https://www.html5rocks.com/en/tutorials/webrtc/basics/
    6     2 
    7    -1 	// ICE -- networking information
    8    -1 	// offer/answer -- media stream information
   -1     3 import * as signal from './signal.js';
    9     4 
   10    -1 	var room = location.hash.substr(1);
   11    -1 	var queue = signal.randomString(10);
   12    -1 	var queuePassword = signal.randomString(10);
   13    -1 	console.log('own queue', queue);
   -1     5 // https://webrtc.github.io/samples/
   -1     6 // https://www.html5rocks.com/en/tutorials/webrtc/basics/
   14     7 
   15    -1 	var container = document.querySelector('.rtc-videos');
   16    -1 	var cons = {};
   -1     8 // ICE -- networking information
   -1     9 // offer/answer -- media stream information
   17    10 
   18    -1 	var localVideo = document.querySelector('.rtc-videos video.local');
   19    -1 	localVideo.srcObject = new MediaStream();
   -1    11 var room = location.hash.substr(1);
   -1    12 var queue = signal.randomString(10);
   -1    13 var queuePassword = signal.randomString(10);
   -1    14 console.log('own queue', queue);
   20    15 
   21    -1 	var closeConnection = function(sender) {
   22    -1 		if (sender in cons) {
   23    -1 			cons[sender].video.remove();
   24    -1 			cons[sender].con.close();
   25    -1 			delete cons[sender];
   26    -1 		}
   27    -1 	};
   -1    16 var container = document.querySelector('.rtc-videos');
   -1    17 var cons = {};
   28    18 
   29    -1 	var getConnection = function(sender) {
   30    -1 		if (sender in cons) {
   31    -1 			return cons[sender].con;
   32    -1 		}
   -1    19 var localVideo = document.querySelector('.rtc-videos video.local');
   -1    20 localVideo.srcObject = new MediaStream();
   33    21 
   34    -1 		var video = document.createElement('video');
   35    -1 		video.autoplay = true;
   36    -1 		container.append(video);
   -1    22 var closeConnection = function(sender) {
   -1    23 	if (sender in cons) {
   -1    24 		cons[sender].video.remove();
   -1    25 		cons[sender].con.close();
   -1    26 		delete cons[sender];
   -1    27 	}
   -1    28 };
   37    29 
   38    -1 		var con = new RTCPeerConnection({
   39    -1 			iceServers: [{urls: 'stun:ce9e.org:3478'}],
   40    -1 		});
   41    -1 		con.addEventListener('icecandidate', function(event) {
   42    -1 			signal.post(sender, {sender: queue, type: 'candidate', data: event.candidate});
   43    -1 		});
   44    -1 		con.addEventListener('negotiationneeded', function() {
   45    -1 			makeOffer(sender);
   46    -1 		});
   47    -1 		con.addEventListener('iceconnectionstatechange', function() {
   48    -1 			if (con.iceConnectionState === 'disconnected') {
   49    -1 				closeConnection(sender);
   50    -1 			}
   51    -1 		});
   52    -1 		con.addEventListener('track', function(event) {
   53    -1 			// TODO: maybe check if already equal?
   54    -1 			video.srcObject = event.streams[0];
   55    -1 		});
   -1    30 var getConnection = function(sender) {
   -1    31 	if (sender in cons) {
   -1    32 		return cons[sender].con;
   -1    33 	}
   56    34 
   57    -1 		localVideo.srcObject.getTracks().forEach(track => {
   58    -1 			con.addTrack(track, localVideo.srcObject);
   59    -1 		});
   60    -1 
   61    -1 		cons[sender] = {
   62    -1 			'con': con,
   63    -1 			'video': video,
   64    -1 		};
   -1    35 	var video = document.createElement('video');
   -1    36 	video.autoplay = true;
   -1    37 	container.append(video);
   65    38 
   66    -1 		return con;
   67    -1 	};
   68    -1 
   69    -1 	var makeOffer = function(sender) {
   70    -1 		if (sender !== queue) {
   71    -1 			var con = getConnection(sender);
   72    -1 			con.createOffer().then(offer => {
   73    -1 				con.setLocalDescription(offer).then(() => {
   74    -1 					signal.post(sender, {sender: queue, type: 'offer', data: offer});
   75    -1 				});
   76    -1 			});
   77    -1 		}
   78    -1 	};
   79    -1 
   80    -1 	var handleOffer = function(sender, offer) {
   81    -1 		var con = getConnection(sender);
   82    -1 		con.setRemoteDescription(offer).then(() => {
   83    -1 			con.createAnswer().then(answer => {
   84    -1 				con.setLocalDescription(answer).then(() => {
   85    -1 					signal.post(sender, {sender: queue, type: 'answer', data: answer});
   86    -1 				});
   87    -1 			});
   88    -1 		});
   89    -1 	};
   90    -1 
   91    -1 	var handleAnswer = function(sender, answer) {
   92    -1 		if (sender in cons) {
   93    -1 			var con = cons[sender].con;
   94    -1 			con.setRemoteDescription(answer);
   95    -1 		}
   96    -1 	};
   97    -1 
   98    -1 	var handleCandidate = function(sender, candidate) {
   99    -1 		if (sender in cons) {
  100    -1 			var con = cons[sender].con;
  101    -1 			con.addIceCandidate(candidate);
  102    -1 		}
  103    -1 	};
  104    -1 
  105    -1 	signal.listen(queue + ':' + queuePassword, function(msg) {
  106    -1 		if (msg.type === 'offer') {
  107    -1 			handleOffer(msg.sender, msg.data);
  108    -1 		} else if (msg.type === 'answer') {
  109    -1 			handleAnswer(msg.sender, msg.data);
  110    -1 		} else if (msg.type === 'candidate') {
  111    -1 			handleCandidate(msg.sender, msg.data);
  112    -1 		} else {
  113    -1 			console.log('unknown message', msg);
   -1    39 	var con = new RTCPeerConnection({
   -1    40 		iceServers: [{urls: 'stun:ce9e.org:3478'}],
   -1    41 	});
   -1    42 	con.addEventListener('icecandidate', function(event) {
   -1    43 		signal.post(sender, {sender: queue, type: 'candidate', data: event.candidate});
   -1    44 	});
   -1    45 	con.addEventListener('negotiationneeded', function() {
   -1    46 		makeOffer(sender);
   -1    47 	});
   -1    48 	con.addEventListener('iceconnectionstatechange', function() {
   -1    49 		if (con.iceConnectionState === 'disconnected') {
   -1    50 			closeConnection(sender);
  114    51 		}
  115    52 	});
   -1    53 	con.addEventListener('track', function(event) {
   -1    54 		// TODO: maybe check if already equal?
   -1    55 		video.srcObject = event.streams[0];
   -1    56 	});
  116    57 
  117    -1 	signal.post(room, {sender: queue, type: 'announce'});
  118    -1 
  119    -1 	window.addEventListener('beforeunload', function() {
  120    -1 		signal.beacon(room, {sender: queue, type: 'leave'});
   -1    58 	localVideo.srcObject.getTracks().forEach(track => {
   -1    59 		con.addTrack(track, localVideo.srcObject);
  121    60 	});
  122    61 
  123    -1 	var updateConnections = function() {
  124    -1 		var sender;
  125    -1 		var tracks = localVideo.srcObject.getTracks();
   -1    62 	cons[sender] = {
   -1    63 		'con': con,
   -1    64 		'video': video,
   -1    65 	};
  126    66 
  127    -1 		for (sender in cons) {
  128    -1 			var con = cons[sender].con;
   -1    67 	return con;
   -1    68 };
  129    69 
  130    -1 			con.getSenders().forEach(s => {
  131    -1 				con.removeTrack(s);
   -1    70 var makeOffer = function(sender) {
   -1    71 	if (sender !== queue) {
   -1    72 		var con = getConnection(sender);
   -1    73 		con.createOffer().then(offer => {
   -1    74 			con.setLocalDescription(offer).then(() => {
   -1    75 				signal.post(sender, {sender: queue, type: 'offer', data: offer});
  132    76 			});
  133    -1 
  134    -1 			tracks.forEach(track => {
  135    -1 				con.addTrack(track, localVideo.srcObject);
   -1    77 		});
   -1    78 	}
   -1    79 };
   -1    80 
   -1    81 var handleOffer = function(sender, offer) {
   -1    82 	var con = getConnection(sender);
   -1    83 	con.setRemoteDescription(offer).then(() => {
   -1    84 		con.createAnswer().then(answer => {
   -1    85 			con.setLocalDescription(answer).then(() => {
   -1    86 				signal.post(sender, {sender: queue, type: 'answer', data: answer});
  136    87 			});
  137    -1 		}
  138    -1 	};
   -1    88 		});
   -1    89 	});
   -1    90 };
   -1    91 
   -1    92 var handleAnswer = function(sender, answer) {
   -1    93 	if (sender in cons) {
   -1    94 		var con = cons[sender].con;
   -1    95 		con.setRemoteDescription(answer);
   -1    96 	}
   -1    97 };
   -1    98 
   -1    99 var handleCandidate = function(sender, candidate) {
   -1   100 	if (sender in cons) {
   -1   101 		var con = cons[sender].con;
   -1   102 		con.addIceCandidate(candidate);
   -1   103 	}
   -1   104 };
   -1   105 
   -1   106 signal.listen(queue + ':' + queuePassword, function(msg) {
   -1   107 	if (msg.type === 'offer') {
   -1   108 		handleOffer(msg.sender, msg.data);
   -1   109 	} else if (msg.type === 'answer') {
   -1   110 		handleAnswer(msg.sender, msg.data);
   -1   111 	} else if (msg.type === 'candidate') {
   -1   112 		handleCandidate(msg.sender, msg.data);
   -1   113 	} else {
   -1   114 		console.log('unknown message', msg);
   -1   115 	}
   -1   116 });
   -1   117 
   -1   118 signal.post(room, {sender: queue, type: 'announce'});
   -1   119 
   -1   120 window.addEventListener('beforeunload', function() {
   -1   121 	signal.beacon(room, {sender: queue, type: 'leave'});
   -1   122 });
   -1   123 
   -1   124 var updateConnections = function() {
   -1   125 	var sender;
   -1   126 	var tracks = localVideo.srcObject.getTracks();
   -1   127 
   -1   128 	for (sender in cons) {
   -1   129 		var con = cons[sender].con;
   -1   130 
   -1   131 		con.getSenders().forEach(s => {
   -1   132 			con.removeTrack(s);
   -1   133 		});
  139   134 
  140    -1 	var controls = document.querySelector('.rtc-controls');
   -1   135 		tracks.forEach(track => {
   -1   136 			con.addTrack(track, localVideo.srcObject);
   -1   137 		});
   -1   138 	}
   -1   139 };
  141   140 
  142    -1 	var updateStreams = async function(event) {
  143    -1 		var tracks = localVideo.srcObject.getTracks();
  144    -1 		var kind = event.target.name === 'audio' ? 'audio' : 'video';
   -1   141 var controls = document.querySelector('.rtc-controls');
  145   142 
  146    -1 		if (!event.target.checked) {
  147    -1 			tracks.forEach(track => {
  148    -1 				if (track.kind === kind) {
  149    -1 					track.enabled = false;
  150    -1 				}
  151    -1 			});
  152    -1 		} else if (
  153    -1 			tracks.filter(t => t.kind === kind).length &&
  154    -1 			!(controls.video.checked && controls.screen.checked)
  155    -1 		) {
  156    -1 			tracks.forEach(track => {
  157    -1 				if (track.kind === kind) {
  158    -1 					track.enabled = true;
  159    -1 				}
  160    -1 			});
  161    -1 		} else {
  162    -1 			var newStream = new MediaStream();
  163    -1 			var stream;
  164    -1 			try {
  165    -1 				if (event.target.name === 'screen') {
  166    -1 					stream = await navigator.mediaDevices.getDisplayMedia();
  167    -1 				} else {
  168    -1 					stream = await navigator.mediaDevices.getUserMedia({[kind]: true});
  169    -1 				}
  170    -1 			} catch (err) {
  171    -1 				event.target.checked = false;
  172    -1 				return;
  173    -1 			}
   -1   143 var updateStreams = async function(event) {
   -1   144 	var tracks = localVideo.srcObject.getTracks();
   -1   145 	var kind = event.target.name === 'audio' ? 'audio' : 'video';
  174   146 
  175    -1 			if (event.target.name === 'video') {
  176    -1 				controls.screen.checked = false;
  177    -1 				localVideo.classList.add('mirrored');
  178    -1 			} else if (event.target.name === 'screen') {
  179    -1 				controls.video.checked = false;
  180    -1 				localVideo.classList.remove('mirrored');
   -1   147 	if (!event.target.checked) {
   -1   148 		tracks.forEach(track => {
   -1   149 			if (track.kind === kind) {
   -1   150 				track.enabled = false;
  181   151 			}
   -1   152 		});
   -1   153 	} else if (
   -1   154 		tracks.filter(t => t.kind === kind).length &&
   -1   155 		!(controls.video.checked && controls.screen.checked)
   -1   156 	) {
   -1   157 		tracks.forEach(track => {
   -1   158 			if (track.kind === kind) {
   -1   159 				track.enabled = true;
   -1   160 			}
   -1   161 		});
   -1   162 	} else {
   -1   163 		var newStream = new MediaStream();
   -1   164 		var stream;
   -1   165 		try {
   -1   166 			if (event.target.name === 'screen') {
   -1   167 				stream = await navigator.mediaDevices.getDisplayMedia();
   -1   168 			} else {
   -1   169 				stream = await navigator.mediaDevices.getUserMedia({[kind]: true});
   -1   170 			}
   -1   171 		} catch (err) {
   -1   172 			event.target.checked = false;
   -1   173 			return;
   -1   174 		}
  182   175 
  183    -1 			tracks.forEach(track => {
  184    -1 				if (track.kind === kind) {
  185    -1 					track.stop();
  186    -1 				} else {
  187    -1 					newStream.addTrack(track);
  188    -1 				}
  189    -1 			});
  190    -1 			stream.getTracks().forEach(track => {
  191    -1 				newStream.addTrack(track);
  192    -1 			});
  193    -1 
  194    -1 			localVideo.srcObject = newStream;
  195    -1 			updateConnections();
   -1   176 		if (event.target.name === 'video') {
   -1   177 			controls.screen.checked = false;
   -1   178 			localVideo.classList.add('mirrored');
   -1   179 		} else if (event.target.name === 'screen') {
   -1   180 			controls.video.checked = false;
   -1   181 			localVideo.classList.remove('mirrored');
  196   182 		}
  197    -1 	};
  198   183 
  199    -1 	document.querySelector('.rtc-controls').addEventListener('change', updateStreams);
   -1   184 		tracks.forEach(track => {
   -1   185 			if (track.kind === kind) {
   -1   186 				track.stop();
   -1   187 			} else {
   -1   188 				newStream.addTrack(track);
   -1   189 			}
   -1   190 		});
   -1   191 		stream.getTracks().forEach(track => {
   -1   192 			newStream.addTrack(track);
   -1   193 		});
  200   194 
   -1   195 		localVideo.srcObject = newStream;
   -1   196 		updateConnections();
   -1   197 	}
   -1   198 };
  201   199 
  202    -1 	var history = document.querySelector('.chat-history');
  203    -1 	var form = document.querySelector('.chat-form');
   -1   200 document.querySelector('.rtc-controls').addEventListener('change', updateStreams);
  204   201 
  205    -1 	form.addEventListener('submit', function(event) {
  206    -1 		var input = event.target.msg;
  207    -1 		event.preventDefault();
  208    -1 		if (!input.value) {
  209    -1 			return;
  210    -1 		}
  211    -1 		signal.post(room, {sender: queue, type: 'chat', text: input.value}).then(function() {
  212    -1 			input.value = '';
  213    -1 		});
  214    -1 	});
  215   202 
  216    -1 	var addChatMsg = function(msg) {
  217    -1 		var li = document.createElement('li');
  218    -1 		li.textContent = msg;
  219    -1 		history.append(li);
  220    -1 		history.scrollTop = history.scrollHeight;
  221    -1 	};
   -1   203 var history = document.querySelector('.chat-history');
   -1   204 var form = document.querySelector('.chat-form');
  222   205 
  223    -1 	signal.listen(room, function(msg) {
  224    -1 		if (msg.type === 'chat') {
  225    -1 			addChatMsg(msg.text);
  226    -1 		} else if (msg.type === 'announce') {
  227    -1 			makeOffer(msg.sender);
  228    -1 		} else if (msg.type === 'leave') {
  229    -1 			closeConnection(msg.sender);
  230    -1 		} else {
  231    -1 			console.log('unknown message', msg);
  232    -1 		}
   -1   206 form.addEventListener('submit', function(event) {
   -1   207 	var input = event.target.msg;
   -1   208 	event.preventDefault();
   -1   209 	if (!input.value) {
   -1   210 		return;
   -1   211 	}
   -1   212 	signal.post(room, {sender: queue, type: 'chat', text: input.value}).then(function() {
   -1   213 		input.value = '';
  233   214 	});
  234    -1 
  235    -1 	navigator.mediaDevices.enumerateDevices().then(devices => {
  236    -1 		devices.forEach(device => {
  237    -1 			if (device.kind === 'audioinput') {
  238    -1 				document.querySelector('.rtc-controls [name="audio"]').disabled = false;
  239    -1 			} else if (device.kind === 'videoinput') {
  240    -1 				document.querySelector('.rtc-controls [name="video"]').disabled = false;
  241    -1 			}
  242    -1 		});
   -1   215 });
   -1   216 
   -1   217 var addChatMsg = function(msg) {
   -1   218 	var li = document.createElement('li');
   -1   219 	li.textContent = msg;
   -1   220 	history.append(li);
   -1   221 	history.scrollTop = history.scrollHeight;
   -1   222 };
   -1   223 
   -1   224 signal.listen(room, function(msg) {
   -1   225 	if (msg.type === 'chat') {
   -1   226 		addChatMsg(msg.text);
   -1   227 	} else if (msg.type === 'announce') {
   -1   228 		makeOffer(msg.sender);
   -1   229 	} else if (msg.type === 'leave') {
   -1   230 		closeConnection(msg.sender);
   -1   231 	} else {
   -1   232 		console.log('unknown message', msg);
   -1   233 	}
   -1   234 });
   -1   235 
   -1   236 navigator.mediaDevices.enumerateDevices().then(devices => {
   -1   237 	devices.forEach(device => {
   -1   238 		if (device.kind === 'audioinput') {
   -1   239 			document.querySelector('.rtc-controls [name="audio"]').disabled = false;
   -1   240 		} else if (device.kind === 'videoinput') {
   -1   241 			document.querySelector('.rtc-controls [name="video"]').disabled = false;
   -1   242 		}
  243   243 	});
  244    -1 })();
   -1   244 });

diff --git a/www/signal.js b/www/signal.js

@@ -1,37 +1,28 @@
    1    -1 (function() {
    2    -1 	var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    3    -1 	var baseUrl = 'https://duct.ce9e.org/';
   -1     1 var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
   -1     2 var baseUrl = 'https://duct.ce9e.org/';
    4     3 
    5    -1 	var randomString = function(length) {
    6    -1 		var result = [];
    7    -1 		for (var i = 0; i < length; i++) {
    8    -1 			var k = Math.floor(Math.random() * chars.length);
    9    -1 			result.push(chars[k]);
   10    -1 		}
   11    -1 		return result.join('');
   12    -1 	};
   13    -1 
   14    -1 	if (!location.hash) {
   15    -1 		location.hash = randomString(10);
   -1     4 export var randomString = function(length) {
   -1     5 	var result = [];
   -1     6 	for (var i = 0; i < length; i++) {
   -1     7 		var k = Math.floor(Math.random() * chars.length);
   -1     8 		result.push(chars[k]);
   16     9 	}
   -1    10 	return result.join('');
   -1    11 };
   17    12 
   18    -1 	var post = function(key, data) {
   19    -1 		return fetch(baseUrl + key, {method: 'POST', body: JSON.stringify(data)});
   20    -1 	};
   -1    13 export var post = function(key, data) {
   -1    14 	return fetch(baseUrl + key, {method: 'POST', body: JSON.stringify(data)});
   -1    15 };
   21    16 
   22    -1 	var beacon = function(key, data) {
   23    -1 		return navigator.sendBeacon(baseUrl + key, JSON.stringify(data));
   24    -1 	};
   -1    17 export var beacon = function(key, data) {
   -1    18 	return navigator.sendBeacon(baseUrl + key, JSON.stringify(data));
   -1    19 };
   25    20 
   26    -1 	var listen = function(key, fn) {
   27    -1 		var evtSource = new EventSource(baseUrl + key + '?sse');
   28    -1 		evtSource.onmessage = msg => fn(JSON.parse(msg.data));
   29    -1 	};
   -1    21 export var listen = function(key, fn) {
   -1    22 	var evtSource = new EventSource(baseUrl + key + '?sse');
   -1    23 	evtSource.onmessage = msg => fn(JSON.parse(msg.data));
   -1    24 };
   30    25 
   31    -1 	window.signal = {
   32    -1 		'post': post,
   33    -1 		'beacon': beacon,
   34    -1 		'listen': listen,
   35    -1 		'randomString': randomString,
   36    -1 	};
   37    -1 })();
   -1    26 if (!location.hash) {
   -1    27 	location.hash = randomString(10);
   -1    28 }