Initial commit of Twilio Server Starter for Python

This commit is contained in:
Jeffrey Linwood
2016-11-14 10:41:18 -06:00
parent 5430cce36c
commit 49b3c6d012
20 changed files with 1259 additions and 2 deletions

72
static/config-check.js Normal file
View File

@@ -0,0 +1,72 @@
$(function() {
$.get('/config', function(response) {
configureField(response, 'TWILIO_ACCOUNT_SID','twilioAccountSID',false);
configureField(response, 'TWILIO_API_KEY','twilioAPIKey',false);
configureField(response, 'TWILIO_API_SECRET','twilioAPISecret',true);
configureField(response, 'TWILIO_CONFIGURATION_SID','twilioConfigurationSID',false);
configureField(response, 'TWILIO_NOTIFICATION_SERVICE_SID','twilioNotificationServiceSID',false);
configureField(response, 'TWILIO_APN_CREDENTIAL_SID','twilioAPNCredentialSID',false);
configureField(response, 'TWILIO_GCM_CREDENTIAL_SID','twilioGCMCredentialSID',false);
configureField(response, 'TWILIO_IPM_SERVICE_SID','twilioIPMServiceSID',false);
configureField(response, 'TWILIO_SYNC_SERVICE_SID','twilioSyncServiceSID',false);
//configure individual product buttons
if (response.TWILIO_ACCOUNT_SID && response.TWILIO_ACCOUNT_SID != '' &&
response.TWILIO_API_KEY && response.TWILIO_API_KEY != '' && response.TWILIO_API_SECRET) {
if (response.TWILIO_CONFIGURATION_SID && response.TWILIO_CONFIGURATION_SID != '') {
$('#videoDemoButton').addClass('btn-success');
} else {
$('#videoDemoButton').addClass('btn-danger');
}
if (response.TWILIO_IPM_SERVICE_SID && response.TWILIO_IPM_SERVICE_SID != '') {
$('#ipmDemoButton').addClass('btn-success');
} else {
$('#ipmDemoButton').addClass('btn-danger');
}
if (response.TWILIO_SYNC_SERVICE_SID && response.TWILIO_SYNC_SERVICE_SID != '') {
$('#syncDemoButton').addClass('btn-success');
} else {
$('#syncDemoButton').addClass('btn-danger');
}
if (response.TWILIO_NOTIFICATION_SERVICE_SID && response.TWILIO_NOTIFICATION_SERVICE_SID != '') {
$('#notifyDemoButton').addClass('btn-success');
} else {
$('#notifyDemoButton').addClass('btn-danger');
}
}
else {
$('#videoDemoButton').addClass('btn-danger');
$('#ipmDemoButton').addClass('btn-danger');
$('#syncDemoButton').addClass('btn-danger');
$('#notifyDemoButton').addClass('btn-danger');
}
});
var configureField = function(response, keyName,elementId,masked) {
if (masked) {
if (response[keyName]) {
$('#' + elementId).html('Configured properly');
$('#' + elementId).addClass('set');
} else {
$('#' + elementId).html('Not configured in .env');
$('#' + elementId).addClass('unset');
}
} else {
if (response[keyName] && response[keyName] != '') {
$('#' + elementId).html(response[keyName]);
$('#' + elementId).addClass('set');
} else {
$('#' + elementId).html('Not configured in .env');
$('#' + elementId).addClass('unset');
}
}
};
});

7
static/index.css Normal file
View File

@@ -0,0 +1,7 @@
.config-value.set {
color:seagreen;
}
.config-value.unset {
color:darkred;
}

74
static/index.html Normal file
View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Twilio Server Starter Kit</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="index.css">
</head>
<body>
<div class="container">
<h1>Twilio Server Starter Kit Environment Setup</h1>
<h2>Account Information</h2>
<table class="table table-striped">
<tr>
<td class="config-key">TWILIO_ACCOUNT_SID</td>
<td class="config-value" id="twilioAccountSID"></td>
</tr>
<tr>
<td class="config-key">TWILIO_API_KEY</td>
<td class="config-value" id="twilioAPIKey"></td>
</tr>
<tr>
<td class="config-key">TWILIO_API_SECRET</td>
<td class="config-value" id="twilioAPISecret"></td>
</tr>
</table>
<h2>Products</h2>
<table class="table table-striped">
<tr>
<td class="config-product">Video</td>
<td class="config-key">TWILIO_CONFIGURATION_SID</td>
<td class="config-value" id="twilioConfigurationSID"></td>
</tr>
<tr>
<td class="config-product">Notify</td>
<td class="config-key">TWILIO_NOTIFICATION_SERVICE_SID</td>
<td class="config-value" id="twilioNotificationServiceSID"></td>
</tr>
<tr>
<td class="config-product">Notify</td>
<td class="config-key">TWILIO_APN_CREDENTIAL_SID</td>
<td class="config-value" id="twilioAPNCredentialSID"></td>
</tr>
<tr>
<td class="config-product">Notify</td>
<td class="config-key">TWILIO_GCM_CREDENTIAL_SID</td>
<td class="config-value" id="twilioGCMCredentialSID"></td>
</tr>
<tr>
<td class="config-product">IP Messaging</td>
<td class="config-key">TWILIO_IPM_SERVICE_SID</td>
<td class="config-value" id="twilioIPMServiceSID"></td>
</tr>
<tr>
<td class="config-product">Sync</td>
<td class="config-key">TWILIO_SYNC_SERVICE_SID</td>
<td class="config-value" id="twilioSyncServiceSID"></td>
</tr>
</table>
<h1>Demos</h1>
<a id="videoDemoButton" class="btn btn-lg" href="/video/">Video</a>
<a id="syncDemoButton" class="btn btn-lg" href="/sync/">Sync</a>
<a id="notifyDemoButton" class="btn btn-lg" href="/notify/">Notify</a>
<a id="ipmDemoButton" class="btn btn-lg" href="/ipmessaging/">IP Messaging</a>
</div> <!-- container -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="/config-check.js"></script>
</body>
</script>

90
static/ipmessaging/index.css Executable file
View File

@@ -0,0 +1,90 @@
* {
box-sizing:border-box;
}
html, body {
padding:0;
margin:0;
height:100%;
width:100%;
color:#dedede;
background-color: #849091;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
}
header {
width:100%;
position:absolute;
text-align:center;
bottom:20px;
}
header a, header a:visited {
font-size:18px;
color:#dedede;
text-decoration:none;
}
header a:hover {
text-decoration:underline;
}
section {
height:70%;
background-color:#2B2B2A;
}
section input {
display:block;
height:52px;
width:800px;
margin:10px auto;
outline:none;
background-color:transparent;
border:none;
border-bottom:1px solid #2B2B2A;
padding:0;
font-size:42px;
color:#eee;
}
#messages {
background-color:#232323;
padding:10px;
height:100%;
width:800px;
margin:0 auto;
overflow-y:auto;
}
#messages p {
margin:5px 0;
padding:0;
}
.info {
margin:5px 0;
font-style:italic;
}
.message-container {
margin:5px 0;
color:#fff;
}
.message-container .username {
display:inline-block;
margin-right:5px;
font-weight:bold;
color:#849091;
}
.me, .username.me {
font-weight:bold;
color:cyan;
}
.message-container .username.me {
display:inline-block;
margin-right:5px;
}

27
static/ipmessaging/index.html Executable file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Twilio IP Messaging Quickstart</title>
<link rel="shortcut icon" href="//www.twilio.com/marketing/bundles/marketing/img/favicons/favicon.ico">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link rel="stylesheet" href="index.css">
</head>
<body>
<header>
<a href="https://www.twilio.com/docs/api/ip-messaging/guides/quickstart-js"
target="_blank">Read the getting started guide
<i class="fa fa-fw fa-external-link"></i>
</a>
</header>
<section>
<div id="messages"></div>
<input id="chat-input" type="text" placeholder="say anything" autofocus/>
</section>
<script src="//media.twiliocdn.com/sdk/js/common/v0.1/twilio-common.min.js"></script>
<script src="//media.twiliocdn.com/sdk/js/ip-messaging/v0.10/twilio-ip-messaging.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="index.js"></script>
</body>
</html>

108
static/ipmessaging/index.js Executable file
View File

@@ -0,0 +1,108 @@
$(function() {
// Get handle to the chat div
var $chatWindow = $('#messages');
// Manages the state of our access token we got from the server
var accessManager;
// Our interface to the IP Messaging service
var messagingClient;
// A handle to the "general" chat channel - the one and only channel we
// will have in this sample app
var generalChannel;
// The server will assign the client a random username - store that value
// here
var username;
// Helper function to print info messages to the chat window
function print(infoMessage, asHtml) {
var $msg = $('<div class="info">');
if (asHtml) {
$msg.html(infoMessage);
} else {
$msg.text(infoMessage);
}
$chatWindow.append($msg);
}
// Helper function to print chat message to the chat window
function printMessage(fromUser, message) {
var $user = $('<span class="username">').text(fromUser + ':');
if (fromUser === username) {
$user.addClass('me');
}
var $message = $('<span class="message">').text(message);
var $container = $('<div class="message-container">');
$container.append($user).append($message);
$chatWindow.append($container);
$chatWindow.scrollTop($chatWindow[0].scrollHeight);
}
// Alert the user they have been assigned a random username
print('Logging in...');
// Get an access token for the current user, passing a username (identity)
// and a device ID - for browser-based apps, we'll always just use the
// value "browser"
$.getJSON('/token', {
device: 'browser'
}, function(data) {
// Alert the user they have been assigned a random username
username = data.identity;
print('You have been assigned a random username of: '
+ '<span class="me">' + username + '</span>', true);
// Initialize the IP messaging client
accessManager = new Twilio.AccessManager(data.token);
messagingClient = new Twilio.IPMessaging.Client(accessManager);
// Get the general chat channel, which is where all the messages are
// sent in this simple application
print('Attempting to join "general" chat channel...');
var promise = messagingClient.getChannelByUniqueName('general');
promise.then(function(channel) {
generalChannel = channel;
if (!generalChannel) {
// If it doesn't exist, let's create it
messagingClient.createChannel({
uniqueName: 'general',
friendlyName: 'General Chat Channel'
}).then(function(channel) {
console.log('Created general channel:');
console.log(channel);
generalChannel = channel;
setupChannel();
});
} else {
console.log('Found general channel:');
console.log(generalChannel);
setupChannel();
}
});
});
// Set up channel after it has been found
function setupChannel() {
// Join the general channel
generalChannel.join().then(function(channel) {
print('Joined channel as '
+ '<span class="me">' + username + '</span>.', true);
});
// Listen for new messages sent to the channel
generalChannel.on('messageAdded', function(message) {
printMessage(message.author, message.body);
});
}
// Send a new message to the general channel
var $input = $('#chat-input');
$input.on('keydown', function(e) {
if (e.keyCode == 13) {
generalChannel.sendMessage($input.val())
$input.val('');
}
});
});

35
static/notify/index.html Normal file
View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<title>Hello App! - Notify Quickstart</title>
<link rel="shortcut icon" href="//www.twilio.com/marketing/bundles/marketing/img/favicons/favicon.ico">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link rel="stylesheet" href="notify.css">
</head>
<body>
<header>
<a href="https://www.twilio.com/docs/api/notifications"
target="_blank">
Read the Twilio Notify guide
<i class="fa fa-fw fa-external-link"></i>
</a>
</header>
<section>
<h1>Send Notification</h1>
<input type="text" id="identityInput" size="30"/>
<p/>
<input type="submit" id="sendNotificationButton" value="Send Notification"/>
<div id="message">
Welcome to Notify!
</div>
<p>After you set up a notification binding, go ahead and send a notification to that identity!</p>
</section>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="notify.js"></script>
</body>
</html>

40
static/notify/notify.css Normal file
View File

@@ -0,0 +1,40 @@
* {
box-sizing:border-box;
}
html, body {
padding:0;
margin:0;
height:100%;
width:100%;
color:#dedede;
background-color: #849091;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
}
header {
width:100%;
position:absolute;
text-align:center;
bottom:20px;
}
header a, header a:visited {
font-size:18px;
color:#dedede;
text-decoration:none;
}
header a:hover {
text-decoration:underline;
}
section {
background-color:#2B2B2A;
text-align:center;
padding:16px;
}
#message {
padding:6px;
}

12
static/notify/notify.js Normal file
View File

@@ -0,0 +1,12 @@
$(function() {
$('#sendNotificationButton').on('click', function() {
$.post('/send-notification', {
identity: $('#identityInput').val()
}, function(response) {
$('#identityInput').val('');
$('#message').html(response.message);
console.log(response);
});
});
});

63
static/sync/index.css Executable file
View File

@@ -0,0 +1,63 @@
* {
box-sizing:border-box;
}
html, body {
padding:0;
margin:0;
height:100%;
width:100%;
color:#dedede;
background-color: #849091;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
}
header {
width:100%;
position:absolute;
text-align:center;
bottom:20px;
}
header a, header a:visited {
font-size:18px;
color:#dedede;
text-decoration:none;
}
header a:hover {
text-decoration:underline;
}
section {
background-color:#2B2B2A;
text-align:center;
padding:16px;
}
button:hover {
cursor:pointer;
background-color:#000;
color:#fff;
}
#message {
padding:6px;
}
#board {
width: 33%;
margin-left: auto;
margin-right: auto;
}
#board .board-row {
width: 100%;
padding-bottom: 3px;
}
#board .board-row button {
width: 30%;
height: 100px;
font-size: 50px;
}

51
static/sync/index.html Executable file
View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<title>Tic-Tac-Twilio - Sync Quickstart</title>
<link rel="shortcut icon" href="//www.twilio.com/marketing/bundles/marketing/img/favicons/favicon.ico">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link rel="stylesheet" href="index.css">
</head>
<body>
<header>
<a href="https://www.twilio.com/docs/api/sync/quickstart-js"
target="_blank">
Read the getting started guide
<i class="fa fa-fw fa-external-link"></i>
</a>
</header>
<section>
<h1>Tic-Tac-Twilio</h1>
<div id="board">
<div class="board-row">
<button type="button" data-row="0" data-col="0" disabled>&nbsp;</button>
<button type="button" data-row="0" data-col="1" disabled>&nbsp;</button>
<button type="button" data-row="0" data-col="2" disabled>&nbsp;</button>
</div>
<div class="board-row">
<button type="button" data-row="1" data-col="0" disabled>&nbsp;</button>
<button type="button" data-row="1" data-col="1" disabled>&nbsp;</button>
<button type="button" data-row="1" data-col="2" disabled>&nbsp;</button>
</div>
<div class="board-row">
<button type="button" data-row="2" data-col="0" disabled>&nbsp;</button>
<button type="button" data-row="2" data-col="1" disabled>&nbsp;</button>
<button type="button" data-row="2" data-col="2" disabled>&nbsp;</button>
</div>
</div>
<div id="message">
Welcome! Initializing Sync...
</div>
<p>Open this page in a few tabs to test!</p>
</section>
<script src="//media.twiliocdn.com/sdk/js/sync/v0.3/twilio-sync.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="index.js"></script>
</body>
</html>

123
static/sync/index.js Executable file
View File

@@ -0,0 +1,123 @@
$(function () {
//We'll use message to tell the user what's happening
var $message = $('#message');
//Get handle to the game board buttons
var $buttons = $('#board .board-row button');
//Manages the state of our access token we got from the server
var accessManager;
//Our interface to the Sync service
var syncClient;
//We're going to use a single Sync document, our simplest
//synchronisation primitive, for this demo
var syncDoc;
//Get an access token for the current user, passing a device ID
//In browser-based apps, every tab is like its own unique device
//synchronizing state -- so we'll use a random UUID to identify
//this tab.
$.getJSON('/token', {
device: getDeviceId()
}, function (tokenResponse) {
//Initialize the Sync client
syncClient = new Twilio.Sync.Client(tokenResponse.token);
//Let's pop a message on the screen to show that Sync is ready
$message.html('Sync initialized!');
//Now that Sync is active, lets enable our game board
$buttons.attr('disabled', false);
//This code will create and/or open a Sync document
//Note the use of promises
syncClient.document('SyncGame').then(function(doc) {
//Lets store it in our global variable
syncDoc = doc;
//Initialize game board UI to current state (if it exists)
var data = syncDoc.get();
if (data.board) {
updateUserInterface(data);
}
//Let's subscribe to changes on this document, so when something
//changes on this document, we can trigger our UI to update
syncDoc.on('updated', updateUserInterface);
});
});
//Whenever a board button is clicked:
$buttons.on('click', function (e) {
//Toggle the value: X, O, or empty
toggleCellValue($(e.target));
//Update the document
var data = readGameBoardFromUserInterface();
//Send updated document to Sync
//This should trigger "updated" events on other clients
syncDoc.set(data);
});
//Toggle the value: X, O, or empty (&nbsp; for UI)
function toggleCellValue($cell) {
var cellValue = $cell.html();
if (cellValue === 'X') {
$cell.html('O');
} else if (cellValue === 'O') {
$cell.html('&nbsp;');
} else {
$cell.html('X');
}
}
//Generate random UUID to identify this browser tab
//For a more robust solution consider a library like
//fingerprintjs2: https://github.com/Valve/fingerprintjs2
function getDeviceId() {
return 'browser-' +
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
//Read the state of the UI and create a new document
function readGameBoardFromUserInterface() {
var board = [
['', '', ''],
['', '', ''],
['', '', '']
];
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
var selector = '[data-row="' + row + '"]' +
'[data-col="' + col + '"]';
board[row][col] = $(selector).html().replace('&nbsp;', '');
}
}
return {board: board};
}
//Update the buttons on the board to match our document
function updateUserInterface(data) {
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
var selector = '[data-row="' + row + '"]' +
'[data-col="' + col + '"]';
var cellValue = data.board[row][col];
$(selector).html(cellValue === '' ? '&nbsp;' : cellValue);
}
}
}
});

29
static/video/index.html Executable file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>Twilio Video - Video Quickstart</title>
<link rel="stylesheet" href="site.css">
</head>
<body>
<div id="remote-media"></div>
<div id="controls">
<div id="preview">
<p class="instructions">Hello Beautiful</p>
<div id="local-media"></div>
<button id="button-preview">Preview My Camera</button>
</div>
<div id="room-controls">
<p class="instructions">Room Name:</p>
<input id="room-name" type="text" placeholder="Enter a room name" />
<button id="button-join">Join Room</button>
<button id="button-leave">Leave Room</button>
</div>
<div id="log"></div>
</div>
<script src="//media.twiliocdn.com/sdk/js/common/v0.1/twilio-common.min.js"></script>
<script src="//media.twiliocdn.com/sdk/js/video/releases/1.0.0-beta2/twilio-video.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="quickstart.js"></script>
</body>
</html>

120
static/video/quickstart.js Executable file
View File

@@ -0,0 +1,120 @@
var videoClient;
var activeRoom;
var previewMedia;
var identity;
var roomName;
// Check for WebRTC
if (!navigator.webkitGetUserMedia && !navigator.mozGetUserMedia) {
alert('WebRTC is not available in your browser.');
}
// When we are about to transition away from this page, disconnect
// from the room, if joined.
window.addEventListener('beforeunload', leaveRoomIfJoined);
$.getJSON('/token', function (data) {
identity = data.identity;
// Create a Conversations Client and connect to Twilio
videoClient = new Twilio.Video.Client(data.token);
document.getElementById('room-controls').style.display = 'block';
// Bind button to join room
document.getElementById('button-join').onclick = function () {
roomName = document.getElementById('room-name').value;
if (roomName) {
log("Joining room '" + roomName + "'...");
videoClient.connect({ to: roomName}).then(roomJoined,
function(error) {
log('Could not connect to Twilio: ' + error.message);
});
} else {
alert('Please enter a room name.');
}
};
// Bind button to leave room
document.getElementById('button-leave').onclick = function () {
log('Leaving room...');
activeRoom.disconnect();
};
});
// Successfully connected!
function roomJoined(room) {
activeRoom = room;
log("Joined as '" + identity + "'");
document.getElementById('button-join').style.display = 'none';
document.getElementById('button-leave').style.display = 'inline';
// Draw local video, if not already previewing
if (!previewMedia) {
room.localParticipant.media.attach('#local-media');
}
room.participants.forEach(function(participant) {
log("Already in Room: '" + participant.identity + "'");
participant.media.attach('#remote-media');
});
// When a participant joins, draw their video on screen
room.on('participantConnected', function (participant) {
log("Joining: '" + participant.identity + "'");
participant.media.attach('#remote-media');
participant.on('disconnected', function (participant) {
log("Participant '" + participant.identity + "' left the room");
});
});
// When a participant disconnects, note in log
room.on('participantDisconnected', function (participant) {
log("Participant '" + participant.identity + "' left the room");
participant.media.detach();
});
// When the conversation ends, stop capturing local video
// Also remove media for all remote participants
room.on('disconnected', function () {
log('Left');
room.localParticipant.media.detach();
room.participants.forEach(function(participant) {
participant.media.detach();
});
activeRoom = null;
document.getElementById('button-join').style.display = 'inline';
document.getElementById('button-leave').style.display = 'none';
});
}
// Local video preview
document.getElementById('button-preview').onclick = function () {
if (!previewMedia) {
previewMedia = new Twilio.Video.LocalMedia();
Twilio.Video.getUserMedia().then(
function (mediaStream) {
previewMedia.addStream(mediaStream);
previewMedia.attach('#local-media');
},
function (error) {
console.error('Unable to access local media', error);
log('Unable to access Camera and Microphone');
});
};
};
// Activity log
function log(message) {
var logDiv = document.getElementById('log');
logDiv.innerHTML += '<p>&gt;&nbsp;' + message + '</p>';
logDiv.scrollTop = logDiv.scrollHeight;
}
function leaveRoomIfJoined() {
if (activeRoom) {
activeRoom.disconnect();
}
}

144
static/video/site.css Executable file
View File

@@ -0,0 +1,144 @@
@import url(https://fonts.googleapis.com/css?family=Share+Tech+Mono);
body,
p {
padding: 0;
margin: 0;
}
body {
background: #272726;
}
div#remote-media {
height: 43%;
width: 100%;
background-color: #fff;
text-align: center;
margin: auto;
}
div#remote-media video {
border: 1px solid #272726;
margin: 3em 2em;
height: 70%;
max-width: 27% !important;
background-color: #272726;
background-repeat: no-repeat;
}
div#controls {
padding: 3em;
max-width: 1200px;
margin: 0 auto;
}
div#controls div {
float: left;
}
div#controls div#room-controls,
div#controls div#preview {
width: 16em;
margin: 0 1.5em;
text-align: center;
}
div#controls p.instructions {
text-align: left;
margin-bottom: 1em;
font-family: Helvetica-LightOblique, Helvetica, sans-serif;
font-style: oblique;
font-size: 1.25em;
color: #777776;
}
div#controls button {
width: 15em;
height: 2.5em;
margin-top: 1.75em;
border-radius: 1em;
font-family: "Helvetica Light", Helvetica, sans-serif;
font-size: .8em;
font-weight: lighter;
outline: 0;
}
div#controls div#room-controls input {
font-family: Helvetica-LightOblique, Helvetica, sans-serif;
font-style: oblique;
font-size: 1em;
}
div#controls button:active {
position: relative;
top: 1px;
}
div#controls div#preview div#local-media {
width: 270px;
height: 202px;
border: 1px solid #cececc;
box-sizing: border-box;
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjgwcHgiIGhlaWdodD0iODBweCIgdmlld0JveD0iMCAwIDgwIDgwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4zLjEgKDEyMDAyKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5GaWxsIDUxICsgRmlsbCA1MjwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPgogICAgICAgIDxnIGlkPSJjdW1tYWNrIiBza2V0Y2g6dHlwZT0iTVNMYXllckdyb3VwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTU5LjAwMDAwMCwgLTE3NDYuMDAwMDAwKSIgZmlsbD0iI0ZGRkZGRiI+CiAgICAgICAgICAgIDxnIGlkPSJGaWxsLTUxLSstRmlsbC01MiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTU5LjAwMDAwMCwgMTc0Ni4wMDAwMDApIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0zOS42ODYsMC43MyBDMTcuODUsMC43MyAwLjA4NSwxOC41IDAuMDg1LDQwLjMzIEMwLjA4NSw2Mi4xNyAxNy44NSw3OS45MyAzOS42ODYsNzkuOTMgQzYxLjUyMiw3OS45MyA3OS4yODcsNjIuMTcgNzkuMjg3LDQwLjMzIEM3OS4yODcsMTguNSA2MS41MjIsMC43MyAzOS42ODYsMC43MyBMMzkuNjg2LDAuNzMgWiBNMzkuNjg2LDEuNzMgQzYxLjAwNSwxLjczIDc4LjI4NywxOS4wMiA3OC4yODcsNDAuMzMgQzc4LjI4Nyw2MS42NSA2MS4wMDUsNzguOTMgMzkuNjg2LDc4LjkzIEMxOC4zNjcsNzguOTMgMS4wODUsNjEuNjUgMS4wODUsNDAuMzMgQzEuMDg1LDE5LjAyIDE4LjM2NywxLjczIDM5LjY4NiwxLjczIEwzOS42ODYsMS43MyBaIiBpZD0iRmlsbC01MSI+PC9wYXRoPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTQ3Ljk2LDUzLjMzNSBMNDcuOTYsNTIuODM1IEwyMC4wOTMsNTIuODM1IEwyMC4wOTMsMjcuODI1IEw0Ny40NiwyNy44MjUgTDQ3LjQ2LDM4LjI1NSBMNTkuMjc5LDMwLjgwNSBMNTkuMjc5LDQ5Ljg1NSBMNDcuNDYsNDIuNDA1IEw0Ny40Niw1My4zMzUgTDQ3Ljk2LDUzLjMzNSBMNDcuOTYsNTIuODM1IEw0Ny45Niw1My4zMzUgTDQ4LjQ2LDUzLjMzNSBMNDguNDYsNDQuMjE1IEw2MC4yNzksNTEuNjY1IEw2MC4yNzksMjguOTk1IEw0OC40NiwzNi40NDUgTDQ4LjQ2LDI2LjgyNSBMMTkuMDkzLDI2LjgyNSBMMTkuMDkzLDUzLjgzNSBMNDguNDYsNTMuODM1IEw0OC40Niw1My4zMzUgTDQ3Ljk2LDUzLjMzNSIgaWQ9IkZpbGwtNTIiPjwvcGF0aD4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+);
background-position: center;
background-repeat: no-repeat;
margin: 0 auto;
}
div#controls div#preview div#local-media video {
max-width: 100%;
max-height: 100%;
border: none;
}
div#controls div#preview button#button-preview {
background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjE3cHgiIGhlaWdodD0iMTJweCIgdmlld0JveD0iMCAwIDE3IDEyIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4zLjEgKDEyMDAyKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5GaWxsIDM0PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9ImN1bW1hY2siIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjUuMDAwMDAwLCAtMTkwOS4wMDAwMDApIiBmaWxsPSIjMEEwQjA5Ij4KICAgICAgICAgICAgPHBhdGggZD0iTTEzNi40NzEsMTkxOS44NyBMMTM2LjQ3MSwxOTE5LjYyIEwxMjUuNzY3LDE5MTkuNjIgTDEyNS43NjcsMTkxMC4wOCBMMTM2LjIyMSwxOTEwLjA4IEwxMzYuMjIxLDE5MTQuMTUgTDE0MC43ODUsMTkxMS4yNyBMMTQwLjc4NSwxOTE4LjQyIEwxMzYuMjIxLDE5MTUuNTUgTDEzNi4yMjEsMTkxOS44NyBMMTM2LjQ3MSwxOTE5Ljg3IEwxMzYuNDcxLDE5MTkuNjIgTDEzNi40NzEsMTkxOS44NyBMMTM2LjcyMSwxOTE5Ljg3IEwxMzYuNzIxLDE5MTYuNDUgTDE0MS4yODUsMTkxOS4zMyBMMTQxLjI4NSwxOTEwLjM3IEwxMzYuNzIxLDE5MTMuMjQgTDEzNi43MjEsMTkwOS41OCBMMTI1LjI2NywxOTA5LjU4IEwxMjUuMjY3LDE5MjAuMTIgTDEzNi43MjEsMTkyMC4xMiBMMTM2LjcyMSwxOTE5Ljg3IEwxMzYuNDcxLDE5MTkuODciIGlkPSJGaWxsLTM0IiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4=)1em center no-repeat #fff;
border: none;
padding-left: 1.5em;
}
div#controls div#log {
border: 1px solid #686865;
}
div#controls div#room-controls {
display: none;
}
div#controls div#room-controls input {
width: 100%;
height: 2.5em;
padding: .5em;
display: block;
}
div#controls div#room-controls button {
color: #fff;
background: 0 0;
border: 1px solid #686865;
}
div#controls div#room-controls button#button-leave {
display: none;
}
div#controls div#log {
width: 35%;
height: 9.5em;
margin-top: 2.75em;
text-align: left;
padding: 1.5em;
float: right;
overflow-y: scroll;
}
div#controls div#log p {
color: #686865;
font-family: 'Share Tech Mono', 'Courier New', Courier, fixed-width;
font-size: 1.25em;
line-height: 1.25em;
margin-left: 1em;
text-indent: -1.25em;
width: 90%;
}