diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b0ad489 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# Required for all uses +TWILIO_ACCOUNT_SID= +TWILIO_API_KEY= +TWILIO_API_SECRET= + +# Required for Video +TWILIO_CONFIGURATION_SID= + +# Required for IP Messaging +TWILIO_IPM_SERVICE_SID= + +# Required for Notify +TWILIO_NOTIFICATION_SERVICE_SID= +TWILIO_APN_CREDENTIAL_SID= +TWILIO_GCM_CREDENTIAL_SID= + +# Required for Sync +TWILIO_SYNC_SERVICE_SID= diff --git a/.gitignore b/.gitignore index 72364f9..9658798 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.env + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index f4c94f9..376dd06 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,92 @@ -# sdk-starter-python -Demo application showcasing Twilio API usage in Python +# Twilio SDK Starter Application for Python + +This sample project demonstrates how to use Twilio APIs in a Python web +application. Once the app is up and running, check out [the home page](http://localhost:5000) +to see which demos you can run. You'll find examples for [IP Messaging](https://www.twilio.com/ip-messaging), +[Video](https://www.twilio.com/video), [Sync](https://www.twilio.com/sync), and more. + +Let's get started! + +## Configure the sample application + +To run the application, you'll need to gather your Twilio account credentials and configure them +in a file named `.env`. To create this file from an example template, do the following in your +Terminal. + +```bash +cp .env.example .env +``` + +Open `.env` in your favorite text editor and configure the following values. + +### Configure account information + +Every sample in the demo requires some basic credentials from your Twilio account. Configure these first. + +| Config Value | Description | +| :------------- |:------------- | +`TWILIO_ACCOUNT_SID` | Your primary Twilio account identifier - find this [in the console here](https://www.twilio.com/console). +`TWILIO_API_KEY` | Used to authenticate - [generate one here](https://www.twilio.com/console/video/dev-tools/api-keys). +`TWILIO_API_SECRET` | Used to authenticate - [just like the above, you'll get one here](https://www.twilio.com/console/video/dev-tools/api-keys). + +#### A Note on API Keys + +When you generate an API key pair at the URLs above, your API Secret will only be shown once - +make sure to save this information in a secure location, or possibly your `~/.bash_profile`. + +### Configure product-specific settings + +Depending on which demos you'd like to run, you'll need to configure a few more values in your +`.env` file. + +| Config Value | Product Demo | Description | +| :------------- |:------------- |:------------- | +`TWILIO_IPM_SERVICE_SID` | IP Messaging | Like a database for your IP Messaging data - [generate one in the console here](https://www.twilio.com/console/ip-messaging/services) +`TWILIO_CONFIGURATION_SID` | Video | Identifier for a set of config properties for your video application - [find yours here](https://www.twilio.com/console/video/profiles) +`TWILIO_SYNC_SERVICE_SID` | Sync (Preview) | Like a database for your Sync data - generate one with the curl command below. +`TWILIO_NOTIFICATION_SERVICE_SID` | Notify (Preview) | You will need to create a Notify service - [generate one here](https://www.twilio.com/console/notify/services) +`TWILIO_APN_CREDENTIAL_SID` | Notify (Preview) | Adds iOS notification ability to your app - [generate one here](https://www.twilio.com/console/notify/credentials). You'll need to provision your APN push credentials to generate this. See [this](https://www.twilio.com/docs/api/ip-messaging/guides/push-notifications-ios) guide on how to do that. (Optional) +`TWILIO_GCM_CREDENTIAL_SID` | Notify (Preview) |Adds Android/GCM notification ability to your app - [generate one here](https://www.twilio.com/console/notify/credentials). You'll need to provision your GCM push credentials to generate this. See [this](https://www.twilio.com/docs/api/ip-messaging/guides/push-notifications-android) guide on how to do that (Optional) + +#### Temporary: Generating a Sync Service Instance + +During the Sync developer preview, you will need to generate Sync service +instances via API until the Console GUI is available. Using the API key pair you +generated above, generate a service instance via REST API with this curl command: + +```bash +curl -X POST https://preview.twilio.com/Sync/Services \ + -d 'FriendlyName=MySyncServiceInstance' \ + -u 'SKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:your_api_secret' +``` + +## Run the sample application + +This application uses the lightweight [Flask Framework](http://flask.pocoo.org/). + +We need to set up your Python environment. Install `virtualenv` via `pip`: + +```bash +pip install virtualenv +``` + +Next, we need to install our depenedencies: + +```bash +virtualenv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +Now we should be all set! Run the application using the `python` command. + +```bash +python app.py +``` + +Your application should now be running at [http://localhost:5000](http://localhost:5000). When you're finished, deactivate your virtual environment using `deactivate`. + +![Home Screen](https://cloud.githubusercontent.com/assets/809856/19532947/673cc7d6-9603-11e6-9a7c-13c0f9ab33b7.png) + +## License +MIT \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..d55056c --- /dev/null +++ b/app.py @@ -0,0 +1,148 @@ +import os +from flask import Flask, jsonify, request +from faker import Factory +from twilio.rest import Client +from twilio.jwt.access_token import AccessToken, SyncGrant, ConversationsGrant, IpMessagingGrant +from dotenv import load_dotenv, find_dotenv +from os.path import join, dirname + + +app = Flask(__name__) +fake = Factory.create() +dotenv_path = join(dirname(__file__), '.env') +load_dotenv(dotenv_path) + +@app.route('/') +def index(): + return app.send_static_file('index.html') + +@app.route('/video/') +def video(): + return app.send_static_file('video/index.html') + +@app.route('/sync/') +def sync(): + return app.send_static_file('sync/index.html') + +@app.route('/notify/') +def notify(): + return app.send_static_file('notify/index.html') + +@app.route('/ipmessaging/') +def ipmessaging(): + return app.send_static_file('ipmessaging/index.html') + +# Basic health check - check environment variables have been configured +# correctly +@app.route('/config') +def config(): + return jsonify( + TWILIO_ACCOUNT_SID=os.environ['TWILIO_ACCOUNT_SID'], + TWILIO_NOTIFICATION_SERVICE_SID=os.environ['TWILIO_NOTIFICATION_SERVICE_SID'], + TWILIO_APN_CREDENTIAL_SID=os.environ['TWILIO_APN_CREDENTIAL_SID'], + TWILIO_GCM_CREDENTIAL_SID=os.environ['TWILIO_GCM_CREDENTIAL_SID'], + TWILIO_API_KEY=os.environ['TWILIO_API_KEY'], + TWILIO_API_SECRET=bool(os.environ['TWILIO_API_SECRET']), + TWILIO_IPM_SERVICE_SID=os.environ['TWILIO_IPM_SERVICE_SID'], + TWILIO_SYNC_SERVICE_SID=os.environ['TWILIO_SYNC_SERVICE_SID'], + TWILIO_CONFIGURATION_SID=os.environ['TWILIO_CONFIGURATION_SID'] + ) + +@app.route('/token') +def token(): + # get credentials for environment variables + account_sid = os.environ['TWILIO_ACCOUNT_SID'] + api_key = os.environ['TWILIO_API_KEY'] + api_secret = os.environ['TWILIO_API_SECRET'] + sync_service_sid = os.environ['TWILIO_SYNC_SERVICE_SID'] + configuration_profile_sid = os.environ['TWILIO_CONFIGURATION_SID'] + ipm_service_sid = os.environ['TWILIO_IPM_SERVICE_SID'] + + + # create a randomly generated username for the client + identity = fake.user_name() + + # Create a unique endpoint ID for the + device_id = request.args.get('device') + endpoint = "TwilioAppDemo:{0}:{1}".format(identity, device_id) + + # Create access token with credentials + token = AccessToken(account_sid, api_key, api_secret, identity) + + # Create a Sync grant and add to token + if sync_service_sid: + sync_grant = SyncGrant(endpoint_id=endpoint, service_sid=sync_service_sid) + token.add_grant(sync_grant) + + # Create a Video grant and add to token + if configuration_profile_sid: + video_grant = ConversationsGrant(configuration_profile_sid=configuration_profile_sid) + token.add_grant(video_grant) + + # Create an IP Messaging grant and add to token + if ipm_service_sid: + ipm_grant = IpMessagingGrant(endpoint_id=endpoint, service_sid=ipm_service_sid) + token.add_grant(ipm_grant) + + # Return token info as JSON + return jsonify(identity=identity, token=token.to_jwt()) + +# Notify - create a device binding from a POST HTTP request +@app.route('/register', methods=['POST']) +def register(): + # get credentials for environment variables + account_sid = os.environ['TWILIO_ACCOUNT_SID'] + api_key = os.environ['TWILIO_API_KEY'] + api_secret = os.environ['TWILIO_API_SECRET'] + service_sid = os.environ['TWILIO_NOTIFICATION_SERVICE_SID'] + + # Initialize the Twilio client + client = Client(api_key, api_secret, account_sid) + + # Body content + content = request.get_json() + + # Get a reference to the notification service + service = client.notify.v1.services(service_sid) + + # Create the binding + binding = service.bindings.create( + endpoint=content["endpoint"], + identity=content["identity"], + binding_type=content["BindingType"], + address=content["Address"]) + + print binding + + # Return success message + return jsonify(message="Binding created!") + +# Notify - send a notification from a POST HTTP request +@app.route('/send-notification', methods=['POST']) +def send_notification(): + # get credentials for environment variables + account_sid = os.environ['TWILIO_ACCOUNT_SID'] + api_key = os.environ['TWILIO_API_KEY'] + api_secret = os.environ['TWILIO_API_SECRET'] + service_sid = os.environ['TWILIO_NOTIFICATION_SERVICE_SID'] + + # Initialize the Twilio client + client = Client(api_key, api_secret, account_sid) + + service = client.notify.v1.services(service_sid) + + # Create a notification for a given identity + identity = request.form.get('identity') + notification = service.notifications.create( + identity=identity, + body='Hello ' + identity + '!' + ) + + return jsonify(message="Notification created!") + +@app.route('/') +def static_file(path): + return app.send_static_file(path) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cc4ed37 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +Flask==0.10.1 +fake-factory==0.5.3 +twilio==6.0.0rc12 +python-dotenv==0.6.0 diff --git a/static/config-check.js b/static/config-check.js new file mode 100644 index 0000000..c47f778 --- /dev/null +++ b/static/config-check.js @@ -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'); + } + } + + }; +}); \ No newline at end of file diff --git a/static/index.css b/static/index.css new file mode 100644 index 0000000..ea7ef29 --- /dev/null +++ b/static/index.css @@ -0,0 +1,7 @@ +.config-value.set { + color:seagreen; +} + +.config-value.unset { + color:darkred; +} \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..ed02de4 --- /dev/null +++ b/static/index.html @@ -0,0 +1,74 @@ + + + + Twilio Server Starter Kit + + + + + + + +
+

Twilio Server Starter Kit Environment Setup

+

Account Information

+ + + + + + + + + + + + + +
TWILIO_ACCOUNT_SID
TWILIO_API_KEY
TWILIO_API_SECRET
+

Products

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VideoTWILIO_CONFIGURATION_SID
NotifyTWILIO_NOTIFICATION_SERVICE_SID
NotifyTWILIO_APN_CREDENTIAL_SID
NotifyTWILIO_GCM_CREDENTIAL_SID
IP MessagingTWILIO_IPM_SERVICE_SID
SyncTWILIO_SYNC_SERVICE_SID
+ +

Demos

+ Video + Sync + Notify + IP Messaging +
+ + + + + + \ No newline at end of file diff --git a/static/ipmessaging/index.css b/static/ipmessaging/index.css new file mode 100755 index 0000000..4020e41 --- /dev/null +++ b/static/ipmessaging/index.css @@ -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; +} \ No newline at end of file diff --git a/static/ipmessaging/index.html b/static/ipmessaging/index.html new file mode 100755 index 0000000..5e93564 --- /dev/null +++ b/static/ipmessaging/index.html @@ -0,0 +1,27 @@ + + + + Twilio IP Messaging Quickstart + + + + + +
+ Read the getting started guide + + +
+ +
+
+ +
+ + + + + + + diff --git a/static/ipmessaging/index.js b/static/ipmessaging/index.js new file mode 100755 index 0000000..5a9d38f --- /dev/null +++ b/static/ipmessaging/index.js @@ -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 = $('
'); + 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 = $('').text(fromUser + ':'); + if (fromUser === username) { + $user.addClass('me'); + } + var $message = $('').text(message); + var $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: ' + + '' + username + '', 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 ' + + '' + username + '.', 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(''); + } + }); +}); \ No newline at end of file diff --git a/static/notify/index.html b/static/notify/index.html new file mode 100644 index 0000000..ad4036f --- /dev/null +++ b/static/notify/index.html @@ -0,0 +1,35 @@ + + + + Hello App! - Notify Quickstart + + + + + +
+ + Read the Twilio Notify guide + + +
+ +
+

Send Notification

+ +

+ + +

+ Welcome to Notify! +
+ +

After you set up a notification binding, go ahead and send a notification to that identity!

+
+ + + + + + diff --git a/static/notify/notify.css b/static/notify/notify.css new file mode 100644 index 0000000..aac2402 --- /dev/null +++ b/static/notify/notify.css @@ -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; +} diff --git a/static/notify/notify.js b/static/notify/notify.js new file mode 100644 index 0000000..7e7eb3a --- /dev/null +++ b/static/notify/notify.js @@ -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); + }); + }); +}); \ No newline at end of file diff --git a/static/sync/index.css b/static/sync/index.css new file mode 100755 index 0000000..06a7d23 --- /dev/null +++ b/static/sync/index.css @@ -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; +} \ No newline at end of file diff --git a/static/sync/index.html b/static/sync/index.html new file mode 100755 index 0000000..2820760 --- /dev/null +++ b/static/sync/index.html @@ -0,0 +1,51 @@ + + + + Tic-Tac-Twilio - Sync Quickstart + + + + + +
+ + Read the getting started guide + + +
+ +
+

Tic-Tac-Twilio

+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+ Welcome! Initializing Sync... +
+ +

Open this page in a few tabs to test!

+
+ + + + + + + diff --git a/static/sync/index.js b/static/sync/index.js new file mode 100755 index 0000000..f0ad9dc --- /dev/null +++ b/static/sync/index.js @@ -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 (  for UI) + function toggleCellValue($cell) { + var cellValue = $cell.html(); + + if (cellValue === 'X') { + $cell.html('O'); + } else if (cellValue === 'O') { + $cell.html(' '); + } 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(' ', ''); + } + } + + 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 === '' ? ' ' : cellValue); + } + } + } + +}); diff --git a/static/video/index.html b/static/video/index.html new file mode 100755 index 0000000..86fad9b --- /dev/null +++ b/static/video/index.html @@ -0,0 +1,29 @@ + + + + Twilio Video - Video Quickstart + + + +
+
+
+

Hello Beautiful

+
+ +
+
+

Room Name:

+ + + +
+
+
+ + + + + + + diff --git a/static/video/quickstart.js b/static/video/quickstart.js new file mode 100755 index 0000000..f670c26 --- /dev/null +++ b/static/video/quickstart.js @@ -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 += '

> ' + message + '

'; + logDiv.scrollTop = logDiv.scrollHeight; +} + +function leaveRoomIfJoined() { + if (activeRoom) { + activeRoom.disconnect(); + } +} \ No newline at end of file diff --git a/static/video/site.css b/static/video/site.css new file mode 100755 index 0000000..ae2c9bf --- /dev/null +++ b/static/video/site.css @@ -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%; + }