├── .envcopy ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── metrics.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Procfile ├── README.md ├── app.js ├── app.json ├── bin └── www ├── package-lock.json ├── package.json ├── public └── stylesheets │ └── style.css ├── routes └── index.js └── views ├── error.jade ├── index.jade ├── layout.jade └── view.jade /.envcopy: -------------------------------------------------------------------------------- 1 | # enter your TokBox api key after the '=' sign below 2 | TOKBOX_API_KEY=your_api_key 3 | 4 | # enter your TokBox api secret after the '=' sign below 5 | TOKBOX_SECRET=your_project_secret 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | sample 4 | 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "es6": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "extends": ["eslint:recommended", "google", "prettier"], 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "plugins": ["deprecation", "prettier"], 14 | "rules": { 15 | "semi": "warn", 16 | "comma-dangle": "warn", 17 | "curly": ["error", "all"], 18 | "indent": ["error", 2], 19 | "object-curly-spacing": [ 20 | "error", 21 | "always", 22 | { 23 | "objectsInObjects": true, 24 | "arraysInObjects": true 25 | } 26 | ], 27 | "require-jsdoc": ["off"], 28 | "operator-linebreak": ["error", "before"], 29 | "max-len": [ 30 | "error", 31 | { 32 | "code": 120, 33 | "ignoreUrls": true, 34 | "ignoreTemplateLiterals": true, 35 | "ignoreRegExpLiterals": true, 36 | "ignorePattern": "^import.+|test" 37 | } 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/metrics.yml: -------------------------------------------------------------------------------- 1 | name: Aggregit 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | recordMetrics: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: michaeljolley/aggregit@v1 12 | with: 13 | githubToken: ${{ secrets.GITHUB_TOKEN }} 14 | project_id: ${{ secrets.project_id }} 15 | private_key: ${{ secrets.private_key }} 16 | client_email: ${{ secrets.client_email }} 17 | firebaseDbUrl: ${{ secrets.firebaseDbUrl }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # dotenv 40 | .env 41 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v6 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | devrel@vonage.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | For anyone looking to get involved to this project, we are glad to hear from you. Here are a few types of contributions 4 | that we would be interested in hearing about. 5 | 6 | - Bug fixes 7 | - If you find a bug, please first report it using Github Issues. 8 | - Issues that have already been identified as a bug will be labelled `bug`. 9 | - If you'd like to submit a fix for a bug, send a Pull Request from your own fork and mention the Issue number. 10 | - Include a test that isolates the bug and verifies that it was fixed. 11 | - New Features 12 | - If you'd like to accomplish something in the library that it doesn't already do, describe the problem in a new Github Issue. 13 | - Issues that have been identified as a feature request will be labelled `enhancement`. 14 | - If you'd like to implement the new feature, please wait for feedback from the project maintainers before spending too much time writing the code. In some cases, `enhancement`s may not align well with the project objectives at the time. 15 | - Tests, Documentation, Miscellaneous 16 | - If you think the test coverage could be improved, the documentation could be clearer, you've got an alternative implementation of something that may have more advantages, or any other change we would still be glad hear about it. 17 | - If its a trivial change, go ahead and send a Pull Request with the changes you have in mind 18 | - If not, open a Github Issue to discuss the idea first. 19 | 20 | ## Requirements 21 | 22 | For a contribution to be accepted: 23 | 24 | - The test suite must be complete and pass 25 | - Code must follow existing styling conventions 26 | - Commit messages must be descriptive. Related issues should be mentioned by number. 27 | 28 | If the contribution doesn't meet these criteria, a maintainer will discuss it with you on the Issue. You can still continue to add more commits to the branch you have sent the Pull Request from. 29 | 30 | ## How To 31 | 32 | 1. Fork this repository on GitHub. 33 | 1. Clone/fetch your fork to your local development machine. 34 | 1. Create a new branch (e.g. `issue-12`, `feat.add_foo`, etc) and check it out. 35 | 1. Make your changes and commit them. (Did the tests pass?) 36 | 1. Push your new branch to your fork. (e.g. `git push myname issue-12`) 37 | 1. Open a Pull Request from your new branch to the original fork's `master` branch. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Lucas Huang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple OpenTok Server App by Node.js 2 | 3 | Tokbox is now known as Vonage 4 | 5 | This simple server app shows you how to use [OpenTok Node Server SDK](https://tokbox.com/developer/sdks/node/) to create OpenTok sessions, generate tokens for those sessions, archive (or record) sessions, and download those archives. 6 | 7 | ## Quick deploy 8 | 9 | ### Heroku 10 | 11 | Heroku is a PaaS (Platform as a Service) that can be used to deploy simple and small applications for free. To easily deploy this repository to Heroku, sign up for a Heroku account and click this button: 12 | 13 | 14 | Deploy 15 | 16 | 17 | Heroku will prompt you to add your OpenTok API key and OpenTok API secret, which you can 18 | obtain at the [TokBox Dashboard](https://dashboard.tokbox.com/keys). 19 | 20 | ### Railway 21 | 22 | [Railway](https://railway.app/) is a deployment platform where you can provision infrastructure, develop with that infrastructure locally, and then deploy to the cloud. 23 | 24 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/lux452?referralCode=jvcjIS) 25 | 26 | Railway will prompt you to add your OpenTok API key and OpenTok API secret, which you can 27 | obtain at the [TokBox Dashboard](https://dashboard.tokbox.com/keys). 28 | 29 | ## Requirements 30 | 31 | - [Node.js](https://nodejs.org/) 32 | 33 | ## Installing & Running on localhost 34 | 35 | 1. Clone the app by running the command 36 | 37 | git clone git@github.com:opentok/learning-opentok-node.git 38 | 39 | 2. `cd` to the root directory. 40 | 3. Run `npm install` command to fetch and install all npm dependecies. 41 | 4. Next, rename the `.envcopy` file located at the root directory to `.env`, and enter in your TokBox api key and secret as indicated: 42 | 43 | ``` 44 | # enter your TokBox api key after the '=' sign below 45 | TOKBOX_API_KEY= 46 | # enter your TokBox secret after the '=' sign below 47 | TOKBOX_SECRET= 48 | ``` 49 | 50 | 5. Run `npm start` to start the app. 51 | 6. Visit the URL in your browser. You should see a JSON response containing the OpenTok API key, session ID, and token. 52 | 53 | ## Exploring the code 54 | 55 | The `routes/index.js` file is the Express routing for the web service. The rest of this tutorial 56 | discusses code in this file. 57 | 58 | In order to navigate clients to a designated meeting spot, we associate the [Session ID](https://tokbox.com/developer/guides/basics/#sessions) to a room name which is easier for people to recognize and pass. For simplicity, we use a local associated array to implement the association where the room name is the key and the [Session ID](https://tokbox.com/developer/guides/basics/#sessions) is the value. For production applications, you may want to configure a persistence (such as a database) to achieve this functionality. 59 | 60 | ### Generate/Retrieve a Session ID 61 | 62 | The `GET /room/:name` route associates an OpenTok session with a "room" name. This route handles the passed room name and performs a check to determine whether the app should generate a new session ID or retrieve a session ID from the local in-memory hash. Then, it generates an OpenTok token for that session ID. Once the API key, session ID, and token are ready, it sends a response with the body set to a JSON object containing the information. 63 | 64 | ```javascript 65 | if (localStorage[roomName]) { 66 | // fetch an existing sessionId 67 | const sessionId = localStorage[roomName]; 68 | 69 | // generate token 70 | token = opentok.generateToken(sessionId); 71 | res.setHeader('Content-Type', 'application/json'); 72 | res.send({ 73 | apiKey: apiKey, 74 | sessionId: sessionId, 75 | token: token, 76 | }); 77 | } else { 78 | // Create a session that will attempt to transmit streams directly between 79 | // clients. If clients cannot connect, the session uses the OpenTok TURN server: 80 | opentok.createSession({ mediaMode: 'relayed' }, function (err, session) { 81 | if (err) { 82 | console.log(err); 83 | res.status(500).send({ error: 'createSession error:', err }); 84 | return; 85 | } 86 | 87 | // store into local 88 | localStorage[roomName] = session.sessionId; 89 | 90 | // generate token 91 | token = opentok.generateToken(session.sessionId); 92 | res.setHeader('Content-Type', 'application/json'); 93 | res.send({ 94 | apiKey: apiKey, 95 | sessionId: session.sessionId, 96 | token: token, 97 | }); 98 | }); 99 | } 100 | ``` 101 | 102 | The `GET /session` routes generates a convenient session for fast establishment of communication. 103 | 104 | ```javascript 105 | router.get('/session', function (req, res, next) { 106 | res.redirect('/room/session'); 107 | }); 108 | ``` 109 | 110 | ### Start an [Archive](https://tokbox.com/developer/guides/archiving/) 111 | 112 | A `POST` request to the `/archive/start` route starts an archive recording of an OpenTok session. 113 | The session ID OpenTok session is passed in as JSON data in the body of the request 114 | 115 | ```javascript 116 | router.post('/archive/start', function (req, res, next) { 117 | const json = req.body; 118 | const sessionId = json['sessionId']; 119 | opentok.startArchive(sessionId, { name: roomName }, function (err, archive) { 120 | if (err) { 121 | console.log(err); 122 | res.status(500).send({ error: 'startArchive error:', err }); 123 | return; 124 | } 125 | res.setHeader('Content-Type', 'application/json'); 126 | res.send(archive); 127 | }); 128 | }); 129 | ``` 130 | 131 | You can only create an archive for sessions that have at least one client connected. Otherwise, 132 | the app will respond with an error. 133 | 134 | ### Stop an Archive 135 | 136 | A `POST` request to the `/archive:archiveId/stop` route stops an archive recording. 137 | The archive ID is returned by call to the `archive/start` endpoint. 138 | 139 | ```javascript 140 | router.post('/archive/:archiveId/stop', function (req, res, next) { 141 | var archiveId = req.params.archiveId; 142 | console.log('attempting to stop archive: ' + archiveId); 143 | opentok.stopArchive(archiveId, function (err, archive) { 144 | if (err) { 145 | console.log(err); 146 | res.status(500).send({ error: 'stopArchive error:', err }); 147 | return; 148 | } 149 | res.setHeader('Content-Type', 'application/json'); 150 | res.send(archive); 151 | }); 152 | }); 153 | ``` 154 | 155 | ### View an Archive 156 | 157 | A `GET` request to `'/archive/:archiveId/view'` redirects the requested clients to 158 | a URL where the archive gets played. 159 | 160 | ```javascript 161 | router.get('/archive/:archiveId/view', function (req, res, next) { 162 | var archiveId = req.params.archiveId; 163 | console.log('attempting to view archive: ' + archiveId); 164 | opentok.getArchive(archiveId, function (err, archive) { 165 | if (err) { 166 | console.log(err); 167 | res.status(500).send({ error: 'viewArchive error:', err }); 168 | return; 169 | } 170 | 171 | if (archive.status == 'available') { 172 | res.redirect(archive.url); 173 | } else { 174 | res.render('view', { title: 'Archiving Pending' }); 175 | } 176 | }); 177 | }); 178 | ``` 179 | 180 | ### Get Archive information 181 | 182 | A `GET` request to `/archive/:archiveId` returns a JSON object that contains all archive properties, including `status`, `url`, `duration`, etc. For more information, see [here](https://tokbox.com/developer/sdks/node/reference/Archive.html). 183 | 184 | ```javascript 185 | router.get('/archive/:archiveId', function (req, res, next) { 186 | var sessionId = req.params.sessionId; 187 | var archiveId = req.params.archiveId; 188 | 189 | // fetch archive 190 | console.log('attempting to fetch archive: ' + archiveId); 191 | opentok.getArchive(archiveId, function (err, archive) { 192 | if (err) { 193 | console.log(err); 194 | res.status(500).send({ error: 'infoArchive error:', err }); 195 | return; 196 | } 197 | 198 | // extract as a JSON object 199 | res.setHeader('Content-Type', 'application/json'); 200 | res.send(archive); 201 | }); 202 | }); 203 | ``` 204 | 205 | ### Fetch multiple Archives 206 | 207 | A `GET` request to `/archive` with optional `count` and `offset` params returns a list of JSON archive objects. For more information, please check [here](https://tokbox.com/developer/sdks/node/reference/OpenTok.html#listArchives). 208 | 209 | Examples: 210 | 211 | ```javascript 212 | GET /archive // fetch up to 1000 archive objects 213 | GET /archive?count=10 // fetch the first 10 archive objects 214 | GET /archive?offset=10 // fetch archives but first 10 archive objetcs 215 | GET /archive?count=10&offset=10 // fetch 10 archive objects starting from 11st 216 | ``` 217 | 218 | ### Start [Captions](https://tokbox.com/developer/guides/live-captions/) 219 | 220 | A `POST` request to the `/captions/start` route starts caption transcribing of an OpenTok session. 221 | The session ID and a token is passed in as JSON data in the body of the request. 222 | 223 | ```javascript 224 | router.post('/captions/start', async function (req, res) { 225 | // With custom expiry (Default 30 days) 226 | const expires = Math.floor(new Date() / 1000) + (24 * 60 * 60); 227 | const projectJWT = projectToken(apiKey, secret, expires); 228 | const captionURL = `${captionsUrl}/${apiKey}/captions`; 229 | 230 | const captionPostBody = { 231 | sessionId: req.body.sessionId, 232 | token: req.body.token, 233 | languageCode: 'en-US', 234 | partialCaptions: 'true', 235 | }; 236 | 237 | try { 238 | captionResponse = await axios.post(captionURL, captionPostBody, { 239 | headers: { 240 | 'X-OPENTOK-AUTH': projectJWT, 241 | 'Content-Type': 'application/json', 242 | }, 243 | }); 244 | } catch (err) { 245 | console.warn(err); 246 | res.status(500); 247 | res.send(`Error starting transcription services: ${err}`); 248 | return; 249 | } 250 | 251 | res.send(captionResponse.data.captionsId); 252 | }); 253 | ``` 254 | ### Stop [Captions](https://tokbox.com/developer/guides/live-captions/) 255 | 256 | A `POST` request to the `/captions/:captionsId/stop` route stops caption transcribing of an OpenTok session. 257 | The captionsID is passed in as a parameter in the URL. 258 | 259 | ```javascript 260 | router.post('/captions/:captionsId/stop', postBodyParser, async (req, res) => { 261 | const captionsId = req.params.captionsId; 262 | 263 | // With custom expiry (Default 30 days) 264 | const expires = Math.floor(new Date() / 1000) + (24 * 60 * 60); 265 | const projectJWT = projectToken(apiKey, secret, expires); 266 | 267 | const captionURL = `${opentokUrl}/${apiKey}/captions/${captionsId}/stop`; 268 | 269 | try { 270 | const captionResponse = await axios.post(captionURL, {}, { 271 | headers: { 272 | 'X-OPENTOK-AUTH': projectJWT, 273 | 'Content-Type': 'application/json', 274 | }, 275 | }); 276 | res.sendStatus(captionResponse.status); 277 | } catch (err) { 278 | console.warn(err); 279 | res.status(500); 280 | res.send(`Error stopping transcription services: ${err}`); 281 | return; 282 | } 283 | }); 284 | ``` 285 | 286 | ## More information 287 | 288 | This sample app does not provide client-side OpenTok functionality 289 | (for connecting to OpenTok sessions and for publishing and subscribing to streams). 290 | It is intended to be used with the OpenTok tutorials for Web, iOS, iOS-Swift, or Android: 291 | 292 | - [Web](https://tokbox.com/developer/tutorials/web/basic-video-chat/) 293 | - [iOS](https://tokbox.com/developer/tutorials/ios/basic-video-chat/) 294 | - [iOS-Swift](https://tokbox.com/developer/tutorials/ios/swift/basic-video-chat/) 295 | - [Android](https://tokbox.com/developer/tutorials/android/basic-video-chat/) 296 | 297 | ## Development and Contributing 298 | 299 | Interested in contributing? We :heart: pull requests! See the [Contribution](CONTRIBUTING.md) guidelines. 300 | 301 | ## Getting Help 302 | 303 | We love to hear from you so if you have questions, comments or find a bug in the project, let us know! You can either: 304 | 305 | - Open an issue on this repository 306 | - See for support options 307 | - Tweet at us! We're [@VonageDev](https://twitter.com/VonageDev) on Twitter 308 | - Or [join the Vonage Developer Community Slack](https://developer.nexmo.com/community/slack) 309 | 310 | ## Further Reading 311 | 312 | - Check out the Developer Documentation at 313 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const favicon = require('serve-favicon'); 4 | const logger = require('morgan'); 5 | const cookieParser = require('cookie-parser'); 6 | const bodyParser = require('body-parser'); 7 | const fs = require('fs'); 8 | 9 | const index = require('./routes/index'); 10 | const cors = require('cors'); 11 | const app = express(); 12 | 13 | // view engine setup 14 | app.set('views', path.join(__dirname, 'views')); 15 | app.set('view engine', 'jade'); 16 | 17 | app.use(cors()); 18 | 19 | const faviconPath = path.join(__dirname, 'public', 'favicon.ico'); 20 | if (fs.existsSync(faviconPath)) { 21 | app.use(favicon(faviconPath)); 22 | } 23 | app.use(logger('dev')); 24 | app.use(bodyParser.json()); 25 | app.use(bodyParser.urlencoded({ extended: false })); 26 | app.use(cookieParser()); 27 | app.use(express.static(path.join(__dirname, 'public'))); 28 | 29 | app.use('/', index); 30 | 31 | // catch 404 and forward to error handler 32 | app.use(function (req, res, next) { 33 | const err = new Error('Not Found'); 34 | err.status = 404; 35 | next(err); 36 | }); 37 | 38 | // error handler 39 | app.use(function (err, req, res, next) { 40 | // set locals, only providing error in development 41 | res.locals.message = err.message; 42 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 43 | res.locals.title = err.message; 44 | 45 | // render the error page 46 | res.status(err.status || 500); 47 | res.render('error'); 48 | }); 49 | 50 | module.exports = app; 51 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Learning OpenTok Node.js", 3 | "description": "An OpenTok sample app for handling OpenTok server-side functionality on a Node.js server", 4 | "keywords": ["WebRTC", "OpenTok", "video", "chat", "communications"], 5 | "website": "https://tokbox.com", 6 | "repository": "https://github.com/opentok/learning-opentok-node", 7 | "env": { 8 | "TOKBOX_API_KEY": { 9 | "description": "OpenTok API key: Login to the TokBox Dashboard (https://dashboard.tokbox.com/keys) to get this value", 10 | "required": true 11 | }, 12 | "TOKBOX_SECRET": { 13 | "description": "OpenTok API secret: Login to the TokBox Dashboard (https://dashboard.tokbox.com/keys) to get this value", 14 | "required": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | require('dotenv').config(); 7 | var app = require('../app'); 8 | var debug = require('debug')('learning-opentok-node:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '8080'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | console.log('server listening on port:', addr.port); 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learning-opentok-node", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "format": "prettier -w", 7 | "lint": "eslint .", 8 | "lint-fix": "eslint --fix .", 9 | "start": "node ./bin/www" 10 | }, 11 | "dependencies": { 12 | "axios": "1.3.4", 13 | "body-parser": "^1.19.0", 14 | "cookie-parser": "~1.4.5", 15 | "cors": "^2.8.5", 16 | "debug": "~4.3.1", 17 | "dotenv": "^8.2.0", 18 | "eslint-config-google": "0.14.0", 19 | "eslint-config-prettier": "8.5.0", 20 | "eslint-plugin-import": "^2.22.1", 21 | "express": "^4.17.1", 22 | "jade": "^0.29.0", 23 | "lodash": "^4.17.21", 24 | "morgan": "^1.10.0", 25 | "opentok": "^2.11.0", 26 | "opentok-jwt": "0.1.5", 27 | "prettier": "2.8.1", 28 | "prettier-eslint": "15.0.1", 29 | "serve-favicon": "~2.5.0", 30 | "uglify-js": "^3.13.4" 31 | }, 32 | "devDependencies": { 33 | "eslint-plugin-deprecation": "^1.3.3", 34 | "eslint-plugin-prettier": "^4.2.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px 'Lucida Grande', Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00b7ff; 8 | } 9 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | // eslint-disable-next-line new-cap 3 | const router = express.Router(); 4 | const path = require('path'); 5 | const axios = require('axios'); 6 | const { projectToken } = require('opentok-jwt'); 7 | const _ = require('lodash'); 8 | const bodyParser = require('body-parser'); 9 | 10 | const apiKey = process.env.TOKBOX_API_KEY; 11 | const secret = process.env.TOKBOX_SECRET; 12 | 13 | const opentokUrl = 'https://api.opentok.com/v2/project'; 14 | 15 | const postBodyParser = bodyParser.json(); 16 | bodyParser.raw(); 17 | 18 | if (!apiKey || !secret) { 19 | console.error('='.repeat('80')); 20 | console.error(''); 21 | console.error('Missing TOKBOX_API_KEY or TOKBOX_SECRET'); 22 | console.error( 23 | 'Find the appropriate values for these by logging into your TokBox Dashboard at: https://tokbox.com/account/#/', 24 | ); 25 | console.error('Then add them to ', path.resolve('.env'), 'or as environment variables'); 26 | console.error(''); 27 | console.error('='.repeat('80')); 28 | process.exit(); 29 | } 30 | 31 | const OpenTok = require('opentok'); 32 | const opentok = new OpenTok(apiKey, secret); 33 | 34 | // IMPORTANT: roomToSessionIdDictionary is a variable that associates room names with 35 | // unique session IDs. However, since this is stored in memory, restarting your server will 36 | // reset these values. If you want to have a room-to-session association in your production 37 | // application you should consider a more persistent storage 38 | 39 | const roomToSessionIdDictionary = {}; 40 | 41 | // returns the room name, given a session ID that was associated with it 42 | function findRoomFromSessionId(sessionId) { 43 | return _.findKey(roomToSessionIdDictionary, function (value) { 44 | return value === sessionId; 45 | }); 46 | } 47 | 48 | router.get('/', function (req, res) { 49 | res.render('index', { title: 'Learning-OpenTok-Node' }); 50 | }); 51 | 52 | /** 53 | * GET /session redirects to /room/session 54 | */ 55 | router.get('/session', function (req, res) { 56 | res.redirect('/room/session'); 57 | }); 58 | 59 | /** 60 | * GET /room/:name 61 | */ 62 | router.get('/room/:name', function (req, res) { 63 | const roomName = req.params.name; 64 | let sessionId; 65 | let token; 66 | 67 | const tokenOptions = {}; 68 | // we need caption to be moderator role for captions to work 69 | tokenOptions.role = "moderator"; 70 | 71 | console.log('attempting to create a session associated with the room: ' + roomName); 72 | 73 | // if the room name is associated with a session ID, fetch that 74 | if (roomToSessionIdDictionary[roomName]) { 75 | sessionId = roomToSessionIdDictionary[roomName]; 76 | 77 | // generate token 78 | token = opentok.generateToken(sessionId, tokenOptions); 79 | res.setHeader('Content-Type', 'application/json'); 80 | res.send({ 81 | apiKey: apiKey, 82 | sessionId: sessionId, 83 | token: token, 84 | }); 85 | } 86 | // if this is the first time the room is being accessed, create a new session ID 87 | else { 88 | opentok.createSession({ mediaMode: 'routed' }, function (err, session) { 89 | if (err) { 90 | console.log(err); 91 | res.status(500).send({ error: 'createSession error:' + err }); 92 | return; 93 | } 94 | 95 | // now that the room name has a session associated wit it, store it in memory 96 | // IMPORTANT: Because this is stored in memory, restarting your server will reset these values 97 | // if you want to store a room-to-session association in your production application 98 | // you should use a more persistent storage for them 99 | roomToSessionIdDictionary[roomName] = session.sessionId; 100 | 101 | // generate token 102 | token = opentok.generateToken(session.sessionId, tokenOptions); 103 | res.setHeader('Content-Type', 'application/json'); 104 | res.send({ 105 | apiKey: apiKey, 106 | sessionId: session.sessionId, 107 | token: token, 108 | }); 109 | }); 110 | } 111 | }); 112 | 113 | router.post('/captions/start', async (req, res) => { 114 | const sessionId = req.body.sessionId; 115 | 116 | // With custom expiry (Default 30 days) 117 | const expires = Math.floor(new Date() / 1000) + (24 * 60 * 60); 118 | const projectJWT = projectToken(apiKey, secret, expires); 119 | const captionURL = `${opentokUrl}/${apiKey}/captions`; 120 | 121 | const captionPostBody = { 122 | sessionId, 123 | token: req.body.token, 124 | languageCode: 'en-US', 125 | partialCaptions: 'true', 126 | }; 127 | 128 | try { 129 | const captionResponse = await axios.post(captionURL, captionPostBody, { 130 | headers: { 131 | 'X-OPENTOK-AUTH': projectJWT, 132 | 'Content-Type': 'application/json', 133 | }, 134 | }); 135 | 136 | const captionsId = captionResponse.data.captionsId; 137 | res.send({ id: captionsId }); 138 | } catch (err) { 139 | console.warn(err); 140 | res.status(500); 141 | res.send(`Error starting transcription services: ${err}`); 142 | return; 143 | } 144 | }); 145 | 146 | router.post('/captions/:captionsId/stop', postBodyParser, async (req, res) => { 147 | const captionsId = req.params.captionsId; 148 | 149 | // With custom expiry (Default 30 days) 150 | const expires = Math.floor(new Date() / 1000) + (24 * 60 * 60); 151 | const projectJWT = projectToken(apiKey, secret, expires); 152 | 153 | const captionURL = `${opentokUrl}/${apiKey}/captions/${captionsId}/stop`; 154 | 155 | try { 156 | const captionResponse = await axios.post(captionURL, {}, { 157 | headers: { 158 | 'X-OPENTOK-AUTH': projectJWT, 159 | 'Content-Type': 'application/json', 160 | }, 161 | }); 162 | res.send({ status: captionResponse.status }); 163 | } catch (err) { 164 | console.warn(err); 165 | res.status(500); 166 | res.send(`Error stopping transcription services: ${err}`); 167 | return; 168 | } 169 | }); 170 | 171 | /** 172 | * POST /archive/start 173 | */ 174 | router.post('/archive/start', function (req, res) { 175 | const json = req.body; 176 | const sessionId = json.sessionId; 177 | opentok.startArchive(sessionId, { name: findRoomFromSessionId(sessionId) }, function (err, archive) { 178 | if (err) { 179 | console.error('error in startArchive'); 180 | console.error(err); 181 | res.status(500).send({ error: 'startArchive error:' + err }); 182 | return; 183 | } 184 | res.setHeader('Content-Type', 'application/json'); 185 | res.send(archive); 186 | }); 187 | }); 188 | 189 | /** 190 | * POST /archive/:archiveId/stop 191 | */ 192 | router.post('/archive/:archiveId/stop', function (req, res) { 193 | const archiveId = req.params.archiveId; 194 | console.log('attempting to stop archive: ' + archiveId); 195 | opentok.stopArchive(archiveId, function (err, archive) { 196 | if (err) { 197 | console.error('error in stopArchive'); 198 | console.error(err); 199 | res.status(500).send({ error: 'stopArchive error:' + err }); 200 | return; 201 | } 202 | res.setHeader('Content-Type', 'application/json'); 203 | res.send(archive); 204 | }); 205 | }); 206 | 207 | /** 208 | * GET /archive/:archiveId/view 209 | */ 210 | router.get('/archive/:archiveId/view', function (req, res) { 211 | const archiveId = req.params.archiveId; 212 | console.log('attempting to view archive: ' + archiveId); 213 | opentok.getArchive(archiveId, function (err, archive) { 214 | if (err) { 215 | console.error('error in getArchive'); 216 | console.error(err); 217 | res.status(500).send({ error: 'getArchive error:' + err }); 218 | return; 219 | } 220 | 221 | if (archive.status === 'available') { 222 | res.redirect(archive.url); 223 | } else { 224 | res.render('view', { title: 'Archiving Pending' }); 225 | } 226 | }); 227 | }); 228 | 229 | /** 230 | * GET /archive/:archiveId 231 | */ 232 | router.get('/archive/:archiveId', function (req, res) { 233 | const archiveId = req.params.archiveId; 234 | 235 | // fetch archive 236 | console.log('attempting to fetch archive: ' + archiveId); 237 | opentok.getArchive(archiveId, function (err, archive) { 238 | if (err) { 239 | console.error('error in getArchive'); 240 | console.error(err); 241 | res.status(500).send({ error: 'getArchive error:' + err }); 242 | return; 243 | } 244 | 245 | // extract as a JSON object 246 | res.setHeader('Content-Type', 'application/json'); 247 | res.send(archive); 248 | }); 249 | }); 250 | 251 | /** 252 | * GET /archive 253 | */ 254 | router.get('/archive', function (req, res) { 255 | const options = {}; 256 | if (req.query.count) { 257 | options.count = req.query.count; 258 | } 259 | if (req.query.offset) { 260 | options.offset = req.query.offset; 261 | } 262 | 263 | // list archives 264 | console.log('attempting to list archives'); 265 | opentok.listArchives(options, function (err, archives) { 266 | if (err) { 267 | console.error('error in listArchives'); 268 | console.error(err); 269 | res.status(500).send({ error: 'infoArchive error:' + err }); 270 | return; 271 | } 272 | 273 | // extract as a JSON object 274 | res.setHeader('Content-Type', 'application/json'); 275 | res.send(archives); 276 | }); 277 | }); 278 | 279 | router.post('/render', async (req, res) => { 280 | // With custom expiry (Default 30 days) 281 | const expires = Math.floor(new Date() / 1000) + (24 * 60 * 60); 282 | const projectJWT = projectToken(apiKey, secret, expires); 283 | const renderURL = `${opentokUrl}/${apiKey}/render`; 284 | 285 | const renderPostBody = { 286 | sessionId: req.body.sessionId, 287 | token: req.body.token, 288 | "url": "https://www.google.com", 289 | maxDuration: 36000, 290 | "resolution": "1280x720", 291 | "properties": { 292 | name: "Composed stream for Live event", 293 | }, 294 | }; 295 | try { 296 | const renderResponse = await axios.post(renderURL, renderPostBody, { 297 | headers: { 298 | 'X-OPENTOK-AUTH': projectJWT, 299 | 'Content-Type': 'application/json', 300 | }, 301 | }); 302 | res.send(renderResponse.data.id); 303 | } catch (err) { 304 | console.warn(err); 305 | res.status(500); 306 | res.send(`Error starting Experience Composer: ${err}`); 307 | return; 308 | } 309 | }); 310 | 311 | router.get('/render/info', async (req, res) => { 312 | const renderId = req.body.id; 313 | 314 | // With custom expiry (Default 30 days) 315 | const expires = Math.floor(new Date() / 1000) + (24 * 60 * 60); 316 | const projectJWT = projectToken(apiKey, secret, expires); 317 | 318 | const renderURL = `${opentokUrl}/${apiKey}/render/${renderId}`; 319 | 320 | try { 321 | const renderResponse = await axios.get(renderURL, { 322 | headers: { 323 | 'X-OPENTOK-AUTH': projectJWT, 324 | 'Content-Type': 'application/json', 325 | }, 326 | }, {}); 327 | res.sendStatus(renderResponse.status); 328 | } catch (err) { 329 | console.warn(err); 330 | res.status(err.status); 331 | res.send(`Error retrieving composer information: ${err}`); 332 | return; 333 | } 334 | }); 335 | 336 | router.get('/render/list', async (req, res) => { 337 | const count = req.body.count; 338 | 339 | // With custom expiry (Default 30 days) 340 | const expires = Math.floor(new Date() / 1000) + (24 * 60 * 60); 341 | const projectJWT = projectToken(apiKey, secret, expires); 342 | 343 | const renderURL = `${opentokUrl}/${apiKey}/render?count=${count}`; 344 | 345 | try { 346 | const renderResponse = await axios.get(renderURL, { 347 | headers: { 348 | 'X-OPENTOK-AUTH': projectJWT, 349 | 'Content-Type': 'application/json', 350 | }, 351 | }, {}); 352 | res.sendStatus(renderResponse.status); 353 | } catch (err) { 354 | console.warn(err); 355 | res.status(err.response.status); 356 | res.send(`Error retrieving composer information: ${err}`); 357 | return; 358 | } 359 | }); 360 | 361 | router.delete('/render/stop', postBodyParser, async (req, res) => { 362 | const renderId = req.body.id; 363 | 364 | // With custom expiry (Default 30 days) 365 | const expires = Math.floor(new Date() / 1000) + (24 * 60 * 60); 366 | const projectJWT = projectToken(apiKey, secret, expires); 367 | const renderURL = `${opentokUrl}/${apiKey}/render/${renderId}/`; 368 | try { 369 | const renderResponse = await axios.delete(renderURL, { 370 | headers: { 371 | 'Content-Type': 'application/json', 372 | 'X-OPENTOK-AUTH': projectJWT, 373 | }, 374 | }, {}); 375 | res.sendStatus(renderResponse.status); 376 | } catch (err) { 377 | console.warn(err); 378 | res.status(err.response.status); 379 | res.send(`Error stopping the composer: ${err}`); 380 | return; 381 | } 382 | }); 383 | 384 | module.exports = router; 385 | -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | meta(charset='utf-8') 5 | title Learning OpenTok Node 6 | style. 7 | p, td { 8 | font-family: Arial; 9 | } 10 | td:first-child { 11 | font-family: Consolas, Courier, monospace; 12 | padding-right: 20px 13 | } 14 | tr { 15 | border: 1px solid black 16 | } 17 | body 18 | p 19 | | This is a sample web service for use with OpenTok. See the OpenTok 20 | a(href='https://github.com/opentok/learning-opentok-node') 21 | | learning-opentok-node 22 | | repo on GitHub. 23 | p 24 | | Resources are defined at the following endpoints: 25 | table 26 | tr 27 | td GET /session 28 | td Return an OpenTok API key, session ID, and token. 29 | tr 30 | td GET /room/:name 31 | td Return an OpenTok API key, session ID, and token associated with a room name. 32 | tr 33 | td POST /captions/start 34 | td Starts captions. 35 | tr 36 | td POST /captions/:captionsId/stop 37 | td Stops captions. 38 | tr 39 | td POST /archive/start 40 | td Start an archive for the specified OpenTok session. 41 | tr 42 | td POST /archive/:archiveId/stop 43 | td Stop the specified archive. 44 | tr 45 | td GET /archive/:archiveId/view 46 | td View the specified archive. 47 | tr 48 | td GET /archive/:archiveId 49 | td Return metadata for the specified archive. 50 | tr 51 | td GET /archive 52 | td Return a list of archives. 53 | a(href='https://tokbox.com/developer/sdks/node/reference/OpenTok.html#listArchives') More Information 54 | td Pagination is enabled by applying either count or offset parameteres. 55 | tr 56 | td POST /render 57 | td Starts an experience composer. 58 | tr 59 | td GET /render/info 60 | td Returns information about the specified composer. 61 | tr 62 | td GET /render/list 63 | td Returns a list of experience composers 64 | tr 65 | td DELETE /render/stop 66 | td Stops an experience composer 67 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /views/view.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | meta(charset='utf-8') 5 | title View Archive 6 | body 7 | h1 Waiting for the archive... 8 | p This page will refresh until the archive status is available. 9 | script. 10 | setTimeout(function() { 11 | document.location.reload(true); 12 | }, 3000); 13 | --------------------------------------------------------------------------------