rtc

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

commit
04da858fe8564c85333bc59443e79e4370b28940
parent
460199e9da003a14c6515c6eaa015f75ed926abb
Author
Tobias Bengfort <bengfort@mpib-berlin.mpg.de>
Date
2020-03-21 16:16
rtc

Diffstat

M common.css 20 ++++++++++++++++----
A rtc/index.html 34 ++++++++++++++++++++++++++++++++++
A rtc/rtc.css 23 +++++++++++++++++++++++
A rtc/rtc.js 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

4 files changed, 241 insertions, 4 deletions


diff --git a/common.css b/common.css

@@ -8,7 +8,8 @@ body {
    8     8 }
    9     9 
   10    10 input,
   11    -1 button {
   -1    11 button,
   -1    12 .btn {
   12    13 	border: 1px solid #aaa;
   13    14 	padding: 0.3em 0.75em;
   14    15 	font-family: inherit;
@@ -18,19 +19,30 @@ button {
   18    19 	min-width: 0;
   19    20 }
   20    21 
   21    -1 button {
   -1    22 button,
   -1    23 .btn {
   22    24 	display: inline-block;
   23    25 	text-align: center;
   24    26 	border-color: #26c;
   25    27 	background: #26c;
   26    28 	color: #fff;
   -1    29 	cursor: pointer;
   27    30 }
   28    31 button:hover,
   29    -1 button:focus {
   -1    32 button:focus,
   -1    33 .btn:hover,
   -1    34 .btn:focus {
   30    35 	border-color: #25a;
   31    36 	background: #25a;
   32    37 }
   33    -1 button:active {
   -1    38 button:active,
   -1    39 .btn:active,
   -1    40 .toggle :checked + .btn {
   34    41 	border-color: blue;
   35    42 	background: blue;
   36    43 }
   -1    44 
   -1    45 .toggle input {
   -1    46 	/* FIXME: hide only visually */
   -1    47 	display: none;
   -1    48 }

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

@@ -0,0 +1,34 @@
   -1     1 <!DOCTYPE html>
   -1     2 <html>
   -1     3 <head>
   -1     4 	<meta charset="utf-8" />
   -1     5 	<title>duct rtc</title>
   -1     6 	<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src https://patchbay.pub">
   -1     7 	<link rel="stylesheet" href="../common.css">
   -1     8 	<link rel="stylesheet" href="rtc.css">
   -1     9 </head>
   -1    10 <body>
   -1    11 	<div class="rtc">
   -1    12 		<div class="videos">
   -1    13 			<video class="local" autoplay muted></video>
   -1    14 		</div>
   -1    15 		<div class="controls">
   -1    16 			<label class="toggle">
   -1    17 				<input type="checkbox" name="audio" autocomplete="off">
   -1    18 				<span class="btn">Audio</span>
   -1    19 			</label>
   -1    20 			<label class="toggle">
   -1    21 				<input type="checkbox" name="video" autocomplete="off">
   -1    22 				<span class="btn">Video</span>
   -1    23 			</label>
   -1    24 			<label class="toggle">
   -1    25 				<input type="checkbox" name="screen" autocomplete="off">
   -1    26 				<span class="btn">Screen</span>
   -1    27 			</label>
   -1    28 		</div>
   -1    29 	</div>
   -1    30 	<script src="../patch.js"></script>
   -1    31 	<script src="rtc.js"></script>
   -1    32 </body>
   -1    33 </html>
   -1    34 

diff --git a/rtc/rtc.css b/rtc/rtc.css

@@ -0,0 +1,23 @@
   -1     1 body {
   -1     2 	text-align: center;
   -1     3 }
   -1     4 
   -1     5 .rtc {
   -1     6 	display: grid;
   -1     7 	grid-template-rows: 1fr min-content;
   -1     8 	grid-gap: 1em;
   -1     9 	min-height: calc(100vh - 2em);
   -1    10 }
   -1    11 
   -1    12 .videos {
   -1    13 	display: grid;
   -1    14 	grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
   -1    15 	align-content: center;
   -1    16 	grid-gap: 1em;
   -1    17 }
   -1    18 
   -1    19 video {
   -1    20 	background-color: black;
   -1    21 	width: 100%;
   -1    22 	max-height: 80vh;
   -1    23 }

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

@@ -0,0 +1,168 @@
   -1     1 (function() {
   -1     2 	// https://webrtc.github.io/samples/
   -1     3 	// https://www.html5rocks.com/en/tutorials/webrtc/basics/
   -1     4 
   -1     5 	// ICE -- networking information
   -1     6 	// offer/answer -- media stream information
   -1     7 
   -1     8 	var roomUrl = 'https://patchbay.pub/pubsub/' + location.hash.substr(1);
   -1     9 	var queueUrl = 'https://patchbay.pub/queue/' + patch.randomString(10);
   -1    10 	console.log('own queue', queueUrl);
   -1    11 
   -1    12 	var container = document.querySelector('.videos');
   -1    13 	var cons = {};
   -1    14 
   -1    15 	var localVideo = document.querySelector('video.local');
   -1    16 	localVideo.srcObject = new MediaStream();
   -1    17 
   -1    18 	var getConnection = function(sender) {
   -1    19 		if (sender in cons) {
   -1    20 			return cons[sender].con;
   -1    21 		}
   -1    22 
   -1    23 		var video = document.createElement('video');
   -1    24 		video.autoplay = true;
   -1    25 		container.append(video);
   -1    26 
   -1    27 		var con = new RTCPeerConnection({
   -1    28 			iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
   -1    29 		});
   -1    30 		con.addEventListener('icecandidate', function(event) {
   -1    31 			patch.post(sender, {'sender': queueUrl, 'data': event.candidate});
   -1    32 		});
   -1    33 		con.addEventListener('track', function(event) {
   -1    34 			// TODO: maybe check if already equal?
   -1    35 			// TODO: rm image if no video stream
   -1    36 			video.srcObject = event.streams[0];
   -1    37 		});
   -1    38 
   -1    39 		var tracks = [];
   -1    40 		localVideo.srcObject.getTracks().forEach(track => {
   -1    41 			var s = con.addTrack(track, localVideo.srcObject);
   -1    42 			tracks.push(s);
   -1    43 		});
   -1    44 
   -1    45 		cons[sender] = {
   -1    46 			'con': con,
   -1    47 			'video': video,
   -1    48 			'tracks': tracks,
   -1    49 		};
   -1    50 
   -1    51 		return con;
   -1    52 	};
   -1    53 
   -1    54 	var makeOffer = function(sender) {
   -1    55 		if (sender !== queueUrl) {
   -1    56 			var con = getConnection(sender);
   -1    57 			con.createOffer().then(offer => {
   -1    58 				con.setLocalDescription(offer).then(_ => {
   -1    59 					patch.post(sender, {'sender': queueUrl, 'data': offer});
   -1    60 				});
   -1    61 			});
   -1    62 		}
   -1    63 	};
   -1    64 
   -1    65 	var handleOffer = function(sender, offer) {
   -1    66 		var con = getConnection(sender);
   -1    67 		con.setRemoteDescription(offer).then(_ => {
   -1    68 			con.createAnswer().then(answer => {
   -1    69 				con.setLocalDescription(answer).then(_ => {
   -1    70 					patch.post(sender, {'sender': queueUrl, 'data': answer});
   -1    71 				});
   -1    72 			});
   -1    73 		});
   -1    74 	};
   -1    75 
   -1    76 	var handleAnswer = function(sender, answer) {
   -1    77 		var con = cons[sender].con;
   -1    78 		con.setRemoteDescription(answer);
   -1    79 	};
   -1    80 
   -1    81 	var handleCandidate = function(sender, candidate) {
   -1    82 		var con = cons[sender].con;
   -1    83 		con.addIceCandidate(candidate);
   -1    84 	};
   -1    85 
   -1    86 	patch.listen(roomUrl, function(data) {
   -1    87 		makeOffer(data);
   -1    88 	});
   -1    89 
   -1    90 	patch.listen(queueUrl, function(data) {
   -1    91 		var sender = data.sender;
   -1    92 		var data = data.data;
   -1    93 		if (sender && data) {
   -1    94 			if (data.type === 'offer') {
   -1    95 				return handleOffer(sender, data);
   -1    96 			} else if (data.type === 'answer') {
   -1    97 				return handleAnswer(sender, data);
   -1    98 			} else if (data.candidate) {
   -1    99 				return handleCandidate(sender, data);
   -1   100 			}
   -1   101 		}
   -1   102 		console.log('unknown message', data);
   -1   103 	});
   -1   104 
   -1   105 	patch.post(roomUrl, queueUrl);
   -1   106 
   -1   107 	var updateConnections = function() {
   -1   108 		var sender, c;
   -1   109 		var tracks = localVideo.srcObject.getTracks();
   -1   110 
   -1   111 		for (sender in cons) {
   -1   112 			var c = cons[sender];
   -1   113 
   -1   114 			while (c.tracks.length) {
   -1   115 				s = c.tracks.pop();
   -1   116 				c.con.removeTrack(s);
   -1   117 			}
   -1   118 
   -1   119 			tracks.forEach(track => {
   -1   120 				s = c.con.addTrack(track, localVideo.srcObject);
   -1   121 				c.tracks.push(s);
   -1   122 			});
   -1   123 
   -1   124 			makeOffer(sender);
   -1   125 		}
   -1   126 	}
   -1   127 
   -1   128 	var updateStreams = async function(event) {
   -1   129 		var oldTracks, getStream, other;
   -1   130 
   -1   131 		if (event.target.name === 'audio') {
   -1   132 			oldTracks = localVideo.srcObject.getAudioTracks();
   -1   133 			getStream = () => navigator.mediaDevices.getUserMedia({audio: true});
   -1   134 		} else if (event.target.name === 'video') {
   -1   135 			oldTracks = localVideo.srcObject.getVideoTracks();
   -1   136 			getStream = () => navigator.mediaDevices.getUserMedia({video: true});
   -1   137 			other = document.querySelector('[name="screen"]');
   -1   138 		} else {
   -1   139 			oldTracks = localVideo.srcObject.getVideoTracks();
   -1   140 			getStream = () => navigator.mediaDevices.getDisplayMedia();
   -1   141 			other = document.querySelector('[name="video"]');
   -1   142 		}
   -1   143 
   -1   144 		oldTracks.forEach(track => {
   -1   145 			track.stop()
   -1   146 			localVideo.srcObject.removeTrack(track);
   -1   147 		});
   -1   148 		if (event.target.checked) {
   -1   149 			try {
   -1   150 				var stream = await getStream();
   -1   151 				stream.getTracks().forEach(track => {
   -1   152 					localVideo.srcObject.addTrack(track);
   -1   153 				});
   -1   154 			} catch {
   -1   155 				event.target.checked = false;
   -1   156 				return;
   -1   157 			}
   -1   158 		}
   -1   159 
   -1   160 		if (other) {
   -1   161 			other.checked = false;
   -1   162 		}
   -1   163 
   -1   164 		updateConnections();
   -1   165 	};
   -1   166 
   -1   167 	document.addEventListener('change', updateStreams);
   -1   168 })();