├── .eslintignore
├── .eslintrc
├── .github
├── pull_request_template.md
└── workflows
│ ├── ci.yml
│ └── codecov.yml
├── .gitignore
├── .npmrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DEVELOPING.md
├── LICENSE
├── README.md
├── lib
├── api.js
├── archiving.js
├── broadcast.js
├── callbacks.js
├── captions.js
├── client.js
├── errors.js
├── generateJwt.js
├── moderation.js
├── opentok.js
├── render.js
├── session.js
├── signaling.js
├── sipInterconnect.js
└── stream.js
├── package-lock.json
├── package.json
├── sample
├── Archiving
│ ├── README.md
│ ├── index.js
│ ├── package.json
│ ├── public
│ │ ├── css
│ │ │ └── sample.css
│ │ ├── img
│ │ │ ├── archiving-off.png
│ │ │ ├── archiving-on-idle.png
│ │ │ └── archiving-on-message.png
│ │ └── js
│ │ │ ├── host.js
│ │ │ └── participant.js
│ └── views
│ │ ├── footer.ejs
│ │ ├── header.ejs
│ │ ├── history.ejs
│ │ ├── host.ejs
│ │ ├── index.ejs
│ │ └── participant.ejs
├── Broadcast
│ ├── README.md
│ ├── index.js
│ ├── package.json
│ ├── public
│ │ ├── css
│ │ │ └── sample.css
│ │ └── js
│ │ │ ├── host.js
│ │ │ └── participant.js
│ └── views
│ │ ├── footer.ejs
│ │ ├── header.ejs
│ │ ├── host.ejs
│ │ ├── index.ejs
│ │ └── participant.ejs
├── HelloWorld
│ ├── README.md
│ ├── index.js
│ ├── package.json
│ ├── public
│ │ └── js
│ │ │ └── helloworld.js
│ └── views
│ │ └── index.ejs
└── SipInterconnect
│ ├── Infographic.jpg
│ ├── README.md
│ ├── app.js
│ ├── config.js
│ ├── package.json
│ ├── public
│ └── stylesheets
│ │ ├── pattern.css
│ │ └── style.css
│ ├── tokbox-logo.png
│ └── views
│ └── index.ejs
└── test
├── .eslintrc
├── archiving-test.js
├── callbacks-test.js
├── captions-test.js
├── helpers.js
├── moderation-test.js
├── opentok-test.js
├── session-test.js
├── shim-test.js
└── signaling-test.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | bower_components
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb-base/legacy",
3 | "rules": {
4 | "no-restricted-syntax": ["off", "ForInStatement"],
5 | "brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
6 | "no-param-reassign": ["off"],
7 | "func-names": ["off"],
8 | "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | #### What is this PR doing?
2 |
3 |
4 | #### How should this be manually tested?
5 |
6 |
7 | #### What are the relevant tickets?
8 |
9 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Vonage
5 |
6 | on:
7 | pull_request:
8 | jobs:
9 | test:
10 | strategy:
11 | matrix:
12 | os: [ubuntu-latest, windows-latest, macos-latest]
13 | node: [16.x, 18.x, 20.x]
14 | runs-on: ${{ matrix.os }}
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Set up Node.js ${{ matrix.node }}
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: ${{ matrix.node }}
21 | - name: Install dependencies
22 | run: npm install
23 | - name: Lint, Compile, Test
24 | run: npm test
25 |
--------------------------------------------------------------------------------
/.github/workflows/codecov.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Codecov Report
5 |
6 | on:
7 | pull_request:
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | strategy:
12 | matrix:
13 | node: [18.x]
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Use Node.js ${{ matrix.node }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node }}
20 | - name: Install dependencies
21 | run: npm install
22 | - name: Create codecov report
23 | run: npm run report-coverage
24 | - name: Run codecov
25 | uses: codecov/codecov-action@v1
26 | with:
27 | files: ./mocha.lcov,./jasmine.lcov
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | .vscode
4 | *.log
5 | sample/*/package-lock.json
6 | out/
7 | .nyc_output
8 | coverage.lcov
9 | mocha.lcov
10 | jasmine.lcov
11 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact=true
2 |
--------------------------------------------------------------------------------
/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
13 | Github Issue.
14 | - Issues that have been identified as a feature request will be labelled `enhancement`.
15 | - If you'd like to implement the new feature, please wait for feedback from the project maintainers before spending
16 | too much time writing the code. In some cases, `enhancement`s may not align well with the project objectives at
17 | the time.
18 | * Tests, Documentation, Miscellaneous
19 | - If you think the test coverage could be improved, the documentation could be clearer, you've got an alternative
20 | implementation of something that may have more advantages, or any other change we would still be glad hear about
21 | it.
22 | - If its a trivial change, go ahead and send a Pull Request with the changes you have in mind
23 | - If not, open a Github Issue to discuss the idea first.
24 |
25 | ## Requirements
26 |
27 | For a contribution to be accepted:
28 |
29 | * The test suite must be complete and pass
30 | * Code must follow existing styling conventions
31 | * Commit messages must be descriptive. Related issues should be mentioned by number.
32 |
33 | If the contribution doesn't meet these criteria, a maintainer will discuss it with you on the Issue. You can still
34 | continue to add more commits to the branch you have sent the Pull Request from.
35 |
36 | ## How To
37 |
38 | 1. Fork this repository on GitHub.
39 | 1. Clone/fetch your fork to your local development machine.
40 | 1. Create a new branch (e.g. `issue-12`, `feat.add_foo`, etc) and check it out.
41 | 1. Make your changes and commit them. (Did the tests pass?)
42 | 1. Push your new branch to your fork. (e.g. `git push myname issue-12`)
43 | 1. Open a Pull Request from your new branch to the original fork's `master` branch.
44 |
45 | ## Developer Guidelines
46 |
47 | See DEVELOPING.md for guidelines for developing this project.
48 |
--------------------------------------------------------------------------------
/DEVELOPING.md:
--------------------------------------------------------------------------------
1 | # Development Guidelines
2 |
3 | This document describes tools, tasks and workflow that one needs to be familiar with in order to effectively maintain
4 | this project. If you use this package within your own software as is but don't plan on modifying it, this guide is
5 | **not** for you.
6 |
7 | ## Tasks
8 |
9 | ### Testing
10 |
11 | This project's tests are written with Mocha and with Jasmine. Common tasks:
12 |
13 | * `npm run test` - run the complete test suite.
14 | * `npm run mochaTest` - run just the mocha tests
15 | * `npm run jasmine_node` - run just the jasmine tests
16 |
17 | ### Releasing
18 |
19 | In order to create a release, the following should be completed in order.
20 |
21 | 1. Ensure all the tests are passing (`pm run test`) and that there is enough test coverage.
22 | 1. Ensure package-lock.json and yarn.lock have been updated and committed if any dependency has been added/removed.
23 | 1. Make sure you are on the `dev` branch of the repository, with all changes merged/committed
24 | already.
25 | 1. Update the version number anywhere it appears in the source code and documentation. See
26 | [Versioning](#versioning) for information about selecting an appropriate version number. Files to
27 | check:
28 | - package.json
29 | 1. Commit the version number change with the message "Update to version x.y.z", substituting the new
30 | version number.
31 | 1. Create a git tag: `git tag -a vx.y.z -m "Release vx.y.z"`
32 | 1. Ensure that you have permission to update the
33 | [opentok npm module](https://www.npmjs.org/package/opentok)
34 | 1. Run `npm publish` to release to npm.
35 | 1. Change the version number for future development by incrementing the patch number (z) adding
36 | "-alpha.1" in the source code (not the documentation). For possible files, see above. Then make
37 | another commit with the message "Begin development on next version".
38 | 1. Push the changes to the source repository: `git push origin dev && git push --tags origin`
39 | 1. Add a description to the [GitHub Releases](https://github.com/opentok/opentok-node/releases) page
40 | with any notable changes.
41 |
42 | ## Workflow
43 |
44 | ### Versioning
45 |
46 | The project uses [semantic versioning](http://semver.org/) as a policy for incrementing version numbers. For planned
47 | work that will go into a future version, there should be a Milestone created in the Github Issues named with the version
48 | number (e.g. "v2.2.1").
49 |
50 | During development the version number should end in "-alpha.x" or "-beta.x", where x is an increasing number starting from 1.
51 |
52 | ### Branches
53 |
54 | * `dev` - the main development branch.
55 | * `master` - reflects the latest stable release.
56 | * `feat.foo` - feature branches. these are used for longer running tasks that cannot be accomplished in one commit.
57 | once merged into master, these branches should be deleted.
58 | * `vx.x.x` - if development for a future version/milestone has begun while master is working towards a sooner
59 | release, this is the naming scheme for that branch. once merged into master, these branches should be deleted.
60 |
61 | ### Tags
62 |
63 | * `vx.x.x` - commits are tagged with a final version number during release.
64 |
65 | ### Issues
66 |
67 | Issues are labelled to help track their progress within the pipeline.
68 |
69 | * no label - these issues have not been triaged.
70 | * `bug` - confirmed bug. aim to have a test case that reproduces the defect.
71 | * `enhancement` - contains details/discussion of a new feature. it may not yet be approved or placed into a
72 | release/milestone.
73 | * `wontfix` - closed issues that were never addressed.
74 | * `duplicate` - closed issue that is the same to another referenced issue.
75 | * `question` - purely for discussion
76 |
77 | ### Management
78 |
79 | When in doubt, find the maintainers and ask.
80 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-2016 TokBox, Inc.
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/api.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch');
2 | const { version } = require('../package.json');
3 | const _ = require('lodash');
4 | const generateJwt = require('./generateJwt');
5 | const errors = require('./errors');
6 | const debug = require('debug')
7 |
8 | const log = debug('@opentok');
9 |
10 | const generateHeaders = (config) => ({
11 | 'User-Agent': `OpenTok-Node-SDK/${version}`,
12 | Accept: 'application/json',
13 | ...(config.callVonage
14 | ? {'Authorization': `Bearer ${generateJwt(config)}`}
15 | : {'X-OPENTOK-AUTH': generateJwt(config)}
16 | ),
17 | });
18 |
19 | exports.api = ({
20 | method,
21 | url,
22 | callback,
23 | body,
24 | form,
25 | headers = {},
26 | config,
27 | }) => {
28 | if (!config) {
29 | throw new Error('OT Config was not passed to API caller');
30 | }
31 |
32 | log(`Calling ${config.callVonage ? 'Vonage Video API' : 'OpenTok API'} `)
33 |
34 | let fetchRequest = {
35 | method: method,
36 | body: body,
37 | headers: {
38 | ...generateHeaders(config),
39 | headers,
40 | },
41 | }
42 |
43 | if (body && ['POST', 'PATCH', 'PUT'].includes(method)) {
44 | log('Have body for request')
45 | fetchRequest.body = JSON.stringify(body);
46 | fetchRequest.headers['Content-type'] = 'application/json';
47 | }
48 |
49 | if (form) {
50 | log('Have a form for request')
51 | fetchRequest.body =new URLSearchParams(form).toString()
52 | fetchRequest.headers['Content-type'] ='application/x-www-form-urlencoded';
53 | }
54 |
55 | log(`Request to ${url}`, fetchRequest);
56 |
57 | Promise.resolve(fetch(url, fetchRequest))
58 | .then(async (response) => {
59 | const bodyText = await response.text();
60 | log('Response headers:', response.headers);
61 | log(`Response Body: ${bodyText}`)
62 | let body = bodyText;
63 |
64 | const [contentType] = (response.headers.get('content-type') || '').split(';');
65 |
66 | switch (contentType) {
67 | case 'application/x-www-form-urlencoded':
68 | body = response.body
69 | ? new URLSearchParams(body)
70 | : '' ;
71 | break;
72 | case 'application/json':
73 | // It appears that sometimes, an empty body is sent with the Content-Type header set
74 | // to application/json. If that happens, just set the body to null
75 | body = body ? JSON.parse(bodyText) : null;
76 | // Assume response is just text which will be passed along no default needed
77 | }
78 |
79 | // If we try calling text again, it will fail since the Buffer has been cleared
80 | Object.assign(response, 'text', new Promise((resolve) => resolve(body)))
81 |
82 | switch (response.status) {
83 | case 401:
84 | case 403:
85 | callback(
86 | new errors.AuthError(),
87 | null,
88 | response,
89 | );
90 | return;
91 | case 404:
92 | callback(
93 | new errors.NotFoundError(),
94 | null,
95 | response,
96 | );
97 | return;
98 | }
99 |
100 | if (response.status >= 200 && response.status < 300) {
101 | callback(null, body, response);
102 | return;
103 | }
104 |
105 | callback(
106 | new errors.RequestError(`Unexpected response from OpenTok: "${JSON.stringify({"message": body.message || bodyText})}"`),
107 | body,
108 | response
109 | );
110 | })
111 | .catch(async (error) => {
112 | callback(error, null, null);
113 | });
114 | };
115 |
--------------------------------------------------------------------------------
/lib/archiving.js:
--------------------------------------------------------------------------------
1 | var errors = require('./errors');
2 | var {api} = require('./api')
3 |
4 | /**
5 | * An object representing an OpenTok archive.
6 | *
7 | * Do not call the new()
constructor. To start recording an archive, call the
8 | * {@link OpenTok#startArchive OpenTok.startArchive()} method.
9 | *
10 | * @property {Number} createdAt
11 | * The time at which the archive was created, in milliseconds since the UNIX epoch.
12 | *
13 | * @property {String} duration
14 | * The duration of the archive, in seconds.
15 | *
16 | * @property {Boolean} hasAudio
17 | * Whether the archive has an audio track (true
) or not (false
).
18 | * You can prevent audio from being recorded by setting
19 | * hasAudio
to false
20 | * in the options
parameter you pass into the
21 | * {@link OpenTok#startArchive OpenTok.startArchive()} method.
22 | *
23 | * @property {Boolean} hasVideo
24 | * Whether the archive has an video track (true
) or not (false
).
25 | * You can prevent video from being recorded by setting
26 | * hasVideo
to false
27 | * in the options
parameter you pass into the
28 | * {@link OpenTok#startArchive OpenTok.startArchive()} method.
29 | *
30 | * @property {String} id
31 | * The archive ID.
32 | *
33 | * @property {String} name
34 | * The name of the archive. If no name was provided when the archive was created, this is set
35 | * to null.
36 | *
37 | * @property {String} streamMode
38 | * The stream mode for the archive. This can be set to one of the the following:
39 | *
40 | *
41 | * - "auto" — streams included in the archive are selected automatically
42 | * (the default).
43 | *
44 | * - "manual" — Specify streams to be included based on calls to the
45 | * {@link OpenTok#addArchiveStream OpenTok.addArchiveStream()} and
46 | * {@link OpenTok#removeArchiveStream OpenTok.removeArchiveStream()} methods.
47 | *
48 | *
49 | * @property {String} outputMode
50 | * The output mode to be generated for this archive, which can be one of the following:
51 | *
52 | * - "composed" -- All streams in the archive are recorded to a single (composed) file.
53 | *
- "individual" -- Each stream in the archive is recorded to its own individual file.
54 | *
55 | *
56 | * See the {@link OpenTok#startArchive OpenTok.startArchive()} method.
57 | *
58 | * @property {String} projectId
59 | * The API key associated with the archive.
60 | *
61 | * @property {String} reason
62 | * For archives with the status "stopped" or "failed", this string describes the reason
63 | * the archive stopped (such as "maximum duration exceeded") or failed.
64 | *
65 | * @property {String} resolution The resolution of the archive (either "640x480", "1280x720"
66 | * or "1920x1080").
67 | * This property is only set for composed archives.
68 | *
69 | * @property {String} sessionId
70 | * The session ID of the OpenTok session associated with this archive.
71 | *
72 | * @property {Number} size
73 | * The size of the MP4 file. For archives that have not been generated, this value is set to 0.
74 | *
75 | * @property {String} status
76 | * The status of the archive, which can be one of the following:
77 | *
78 | * - "available" -- The archive is available for download from the OpenTok cloud.
79 | *
- "expired" -- The archive is no longer available for download from the OpenTok cloud.
80 | *
- "failed" -- The archive recording failed.
81 | *
- "paused" -- The archive is in progress and no clients are publishing streams to
82 | * the session. When an archive is in progress and any client publishes a stream,
83 | * the status is "started". When an archive is "paused", nothing is recorded. When
84 | * a client starts publishing a stream, the recording starts (or resumes). If all clients
85 | * disconnect from a session that is being archived, the status changes to "paused", and
86 | * after 60 seconds the archive recording stops (and the status changes to "stopped").
87 | * - "started" -- The archive started and is in the process of being recorded.
88 | *
- "stopped" -- The archive stopped recording.
89 | *
- "uploaded" -- The archive is available for download from the the upload target
90 | * Amazon S3 bucket or Windows Azure container you set up for your
91 | * OpenTok project.
92 | *
93 | *
94 | * @property {String} url
95 | * The download URL of the available MP4 file. This is only set for an archive with the status set
96 | * to "available"; for other archives, (including archives with the status "uploaded") this
97 | * property is set to null. The download URL is obfuscated, and the file is only available from
98 | * the URL for 10 minutes. To generate a new URL, call the
99 | * {@link OpenTok#getArchive OpenTok.getArchive()} or
100 | * {@link OpenTok#listArchives OpenTok.listArchives()} method.
101 | *
102 | * @property {String} multiArchiveTag
103 | * Set this to support recording multiple archives for the same session simultaneously. Set
104 | * this to a unique string for each simultaneous archive of an ongoing session. You must also
105 | * set this option when manually starting an archive that is automatically archived. Note that
106 | * the multiArchiveTag value is not included in the response for the methods to list archives
107 | * and retrieve archive information. If you do not specify a unique multiArchiveTag, you can
108 | * only record one archive at a time for a given session.
109 | * See
110 | * Simultaneous archives.
111 | *
112 | * @see {@link OpenTok#deleteArchive OpenTok.deleteArchive()}
113 | * @see {@link OpenTok#getArchive OpenTok.getArchive()}
114 | * @see {@link OpenTok#startArchive OpenTok.startArchive()}
115 | * @see {@link OpenTok#stopArchive OpenTok.stopArchive()}
116 | * @see {@link OpenTok#listArchives OpenTok.listArchives()}
117 | *
118 | * @class Archive
119 | */
120 |
121 | function Archive(config, properties) {
122 | var hasProp = {}.hasOwnProperty;
123 | var id = properties.id;
124 | var key;
125 |
126 | for (key in properties) {
127 | if (hasProp.call(properties, key)) {
128 | this[key] = properties[key];
129 | }
130 | }
131 |
132 | /**
133 | * Stops the recording of the archive.
134 | *
135 | * Archives automatically stop recording after 120 minutes or when all clients have disconnected
136 | * from the session being archived.
137 | *
138 | * @param callback {Function} The function to call upon completing the operation. Two arguments
139 | * are passed to the function:
140 | *
141 | *
142 | *
143 | * -
144 | *
error
— An error object (if the call to the method fails).
145 | *
146 | *
147 | * -
148 | *
archive
— The Archive object.
149 | *
150 | *
151 | *
152 | *
153 | * @method #stop
154 | * @memberof Archive
155 | */
156 | this.stop = function (callback) {
157 | exports.stopArchive(config, id, callback);
158 | };
159 |
160 | /**
161 | * Deletes the OpenTok archive.
162 | *
163 | * You can only delete an archive which has a status of "available" or "uploaded". Deleting an
164 | * archive removes its record from the list of archives. For an "available" archive, it also
165 | * removes the archive file, making it unavailable for download.
166 | *
167 | * @param callback {Function} The function to call upon completing the operation. On successfully
168 | * deleting the archive, the function is called with no arguments passed in. On failure, an error
169 | * object is passed into the function.
170 | *
171 | * @method #delete
172 | * @memberof Archive
173 | */
174 | this.delete = function (callback) {
175 | exports.deleteArchive(config, id, callback);
176 | };
177 | }
178 |
179 | exports.listArchives = function (config, options, callback) {
180 | if (typeof options === 'function') {
181 | callback = options;
182 | options = {};
183 | }
184 |
185 | if (typeof callback !== 'function') {
186 | throw new errors.ArgumentError('No callback given to listArchives');
187 | }
188 |
189 | const archiveUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/archive`);
190 |
191 | if (options.offset) {
192 | archiveUrl.searchParams.set(
193 | 'offset',
194 | options.offset,
195 | )
196 | }
197 |
198 | if (options.count) {
199 | archiveUrl.searchParams.set(
200 | 'count',
201 | options.count,
202 | )
203 | }
204 |
205 | if (options.sessionId) {
206 | archiveUrl.searchParams.set(
207 | 'sessionId',
208 | options.sessionId,
209 | )
210 | }
211 |
212 | const parseResponse = (err, body, response) => {
213 | if (err) {
214 | callback(err);
215 | return;
216 | }
217 |
218 | callback(
219 | null,
220 | body?.items.map((item) => new Archive(config, item)) || [],
221 | body.count || 0
222 | );
223 | }
224 |
225 | api({
226 | config: config,
227 | method: 'GET',
228 | url: archiveUrl.toString(),
229 | callback: parseResponse,
230 | });
231 | };
232 |
233 | exports.startArchive = function (ot, config, sessionId, options, callback) {
234 | if (typeof options === 'function') {
235 | callback = options;
236 | options = {};
237 | }
238 |
239 | if (typeof callback !== 'function') {
240 | throw new errors.ArgumentError('No callback given to startArchive');
241 | }
242 |
243 | if (!sessionId) {
244 | callback(new errors.ArgumentError('No session ID given'));
245 | return;
246 | }
247 |
248 | const archiveUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/archive`);
249 |
250 | //const oldApi = function (config, method, path, body, callback) {
251 |
252 | const startArchiveCallback = (err, body, response ) => {
253 | if (err && response.status === 404) {
254 | callback(new errors.ArchiveError('Session not found'));
255 | return;
256 | }
257 |
258 | if (err) {
259 | callback(
260 | err,
261 | response.status === 409
262 | ? new errors.ArchiveError('Recording already in progress or session not using OpenTok Media Router')
263 | : err,
264 | );
265 | return;
266 | }
267 |
268 | if (body.status !== 'started') {
269 | callback(new errors.RequestError('Unexpected response from OpenTok: ' + JSON.stringify(body || { status: response.status, statusMessage: response.statusMessage })));
270 | return;
271 | }
272 |
273 | callback(null, new Archive(config, body));
274 | }
275 |
276 | const requestBody = {
277 | sessionId: sessionId,
278 | ...options,
279 | };
280 |
281 | api({
282 | config: config,
283 | method: 'POST',
284 | url: archiveUrl.toString(),
285 | body: requestBody,
286 | callback: startArchiveCallback
287 | })
288 | };
289 |
290 | exports.stopArchive = function (config, archiveId, callback) {
291 | if (typeof callback !== 'function') {
292 | throw new errors.ArgumentError('No callback given to stopArchive');
293 | }
294 |
295 | if (!archiveId) {
296 | callback(new errors.ArgumentError('No archive ID given'));
297 | return;
298 | }
299 |
300 | const archiveUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/archive/${encodeURIComponent(archiveId)}/stop`);
301 |
302 | const stopArchiveCallback = (err, body, response) => {
303 | if (!err) {
304 | callback(null, new Archive(config, body));
305 | return;
306 | }
307 |
308 | switch (response?.status) {
309 | case 404:
310 | callback(new errors.ArchiveError('Archive not found'));
311 | break;
312 | case 409:
313 | callback(new errors.ArchiveError(body?.message || 'Unknown archive error'));
314 | break;
315 | default:
316 | callback(err);
317 | }
318 | }
319 |
320 | api({
321 | config: config,
322 | method: 'POST',
323 | url: archiveUrl.toString(),
324 | body: {},
325 | callback: stopArchiveCallback,
326 | })
327 | };
328 |
329 | exports.getArchive = function (config, archiveId, callback) {
330 | if (typeof callback !== 'function') {
331 | throw new errors.ArgumentError('No callback given to getArchive');
332 | }
333 |
334 | if (!archiveId) {
335 | callback(new errors.ArgumentError('No archive ID given'));
336 | return;
337 | }
338 |
339 | const archiveUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/archive/${encodeURIComponent(archiveId)}`);
340 |
341 | const getArchiveCallback = (err, body, response) => {
342 | if (err && response.status === 404) {
343 | err.message = 'Archive not found';
344 | }
345 |
346 | if (err) {
347 | callback(err);
348 | return;
349 | }
350 |
351 | callback(null, new Archive(config, body));
352 | }
353 | api({
354 | config: config,
355 | method: 'GET',
356 | url: archiveUrl.toString(),
357 | callback: getArchiveCallback,
358 | });
359 | };
360 |
361 | function patchArchive(config, archiveId, options, callback) {
362 | if (!archiveId) {
363 | callback(new errors.ArgumentError('No Archive ID given'));
364 | return;
365 | }
366 |
367 | if (options.addStream && options.removeStream) {
368 | callback(new errors.ArgumentError('You cannot have both addStream and removeStream'));
369 | }
370 |
371 | if (!options.addStream && !options.removeStream) {
372 | callback(new errors.ArgumentError('Need one of addStream or removeStream'));
373 | }
374 |
375 | // Coerce to boolean
376 | if (options.addStream) {
377 | options.hasAudio = Boolean(options.hasAudio);
378 | options.hasVideo = Boolean(options.hasVideo);
379 | }
380 |
381 | const archiveUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/archive/${encodeURIComponent(archiveId)}/streams`);
382 |
383 | const patchArchiveCallback = (err, body, response) => {
384 | switch (response.status) {
385 | case 400:
386 | callback(new errors.ArgumentError('Invalid request: ' + JSON.stringify(body)));
387 | return;
388 | case 405:
389 | callback(new errors.ArchiveError('Unsupported Stream Mode'));
390 | return;
391 | case 404:
392 | callback(new errors.ArchiveError('Archive or stream not found'));
393 | return;
394 | }
395 |
396 | if (err) {
397 | callback(err);
398 | return;
399 | }
400 | callback(null)
401 | }
402 |
403 | api({
404 | config: config,
405 | method: 'PATCH',
406 | url: archiveUrl.toString(),
407 | callback: patchArchiveCallback,
408 | body: options,
409 | });
410 | }
411 |
412 | exports.addArchiveStream = function (config, archiveId, streamId, archiveOptions, callback) {
413 | if (typeof archiveOptions === 'function') {
414 | callback = archiveOptions;
415 | archiveOptions = {};
416 | }
417 |
418 | if (typeof callback !== 'function') {
419 | throw new errors.ArgumentError('No callback given to addArchiveStream');
420 | }
421 |
422 | patchArchive(
423 | config,
424 | archiveId,
425 | {
426 | hasAudio: archiveOptions.hasAudio ? archiveOptions.hasAudio : true,
427 | hasVideo: archiveOptions.hasVideo ? archiveOptions.hasVideo : true,
428 | addStream: streamId
429 | },
430 | callback,
431 | );
432 | };
433 |
434 | exports.removeArchiveStream = function (config, archiveId, streamId, callback) {
435 | if (typeof callback !== 'function') {
436 | throw new errors.ArgumentError('No callback given to removeArchiveStream');
437 | }
438 |
439 | patchArchive(config, archiveId, { removeStream: streamId }, callback);
440 | };
441 |
442 | exports.deleteArchive = function (config, archiveId, callback) {
443 | if (typeof callback !== 'function') {
444 | throw new errors.ArgumentError('No callback given to deleteArchive');
445 | }
446 |
447 | if (!archiveId) {
448 | callback(new errors.ArgumentError('No archive ID given'));
449 | return;
450 | }
451 |
452 | const archiveUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/archive/${encodeURIComponent(archiveId)}`);
453 |
454 | const deleteArchiveCallback = (err, body, response) => {
455 | if (response.status === 404) {
456 | callback(new errors.ArchiveError('Archive not found'));
457 | return;
458 | }
459 |
460 | callback(err);
461 | }
462 | api({
463 | config: config,
464 | method: 'DELETE',
465 | url: archiveUrl.toString(),
466 | callback: deleteArchiveCallback,
467 | });
468 | };
469 |
--------------------------------------------------------------------------------
/lib/broadcast.js:
--------------------------------------------------------------------------------
1 | /**
2 | * An object representing an OpenTok live streaming broadcast.
3 | *
4 | * Do not call the new()
constructor. To start a live streaming broadcast, call the
5 | * {@link OpenTok#startBroadcast OpenTok.startBroadcast()} method.
6 | *
7 | * @property {String} id
8 | * The broadcast ID.
9 | * @property {String} sessionId
10 | * The session ID of the OpenTok session associated with this broadcast.
11 | * @property {String} projectId
12 | * The API key associated with the broadcast.
13 | * @property {Number} createdAt
14 | * The time at which the broadcast was created, in milliseconds since the UNIX epoch.
15 | * @property {String} resolution
16 | * The resolution of the broadcast: one of the following:
17 | *
18 | * - "640x480"
19 | * - "1280x720"
20 | * - "1920x1080"
21 | * - "480x640"
22 | * - "720x1280"
23 | * - "1080x1920"
24 | *
25 | * You may want to use a portrait aspect ratio for broadcasts that include video streams from
26 | * mobile devices (which often use the portrait aspect ratio). This property is optional.
27 | * @property {Object} broadcastUrls
28 | * An object containing details about the HLS and RTMP broadcasts.
29 | *
30 | *
31 | * -
32 | *
hls
(String) — If you specified an HLS endpoint, the object includes
33 | * an hls property, which is set to the URL for the HLS broadcast. Note this HLS broadcast
34 | * URL points to an index file, an .M3U8-formatted playlist that contains a list of URLs to
35 | * .ts media segment files (MPEG-2 transport stream files). While the URLs of both the
36 | * playlist index file and media segment files are provided as soon as the HTTP response
37 | * is returned, these URLs should not be accessed until 15 – 20 seconds later, after the
38 | * initiation of the HLS broadcast, due to the delay between the HLS broadcast and the live
39 | * streams in the OpenTok session.
40 | * See https://developer.apple.com/library/ios/technotes/tn2288/_index.html for more
41 | * information about the playlist index file and media segment files for HLS.
42 | *
43 | * -
44 | *
hlsStatus
(string) — The possible states for the "hlsStatus" field are the following:
45 | *
46 | * - 'connecting': the OpenTok server is in the process of starting transcoders. This is the initial state
47 | * - 'ready': the OpenTok server has succesfully initialised but CDN is not consuming media
48 | * - 'live': the OpenTok server has succesfully initialised and CDN is consuming media
49 | * - 'ended': the source stream has ended (if DVR is enabled and pre-recorded media is requested then status will transition to 'live')
50 | * - 'error': there is an error in the opentok platform.
51 | *
52 | *
53 | * -
54 | *
rtmp
(Object Array) — If you specified RTMP stream endpoints,
55 | * the object includes an rtmp property. This is an array of objects that include
56 | * information on each of the RTMP streams.
57 | * Each of these objects has the following properties:
58 | *
59 | * id
The ID you assigned to the RTMP stream
60 | * serverUrl
The server URL
61 | * streamName
The stream name
62 | * status
The status of the stream
63 | *
64 | *
65 | *
66 | * @property {Number} maxDuration
67 | * The maximum time allowed for the broadcast, in seconds.
68 | * After this time, the broadcast will be stopped automatically, if it is still started.
69 | *
70 | * @propert { Number } maxBitRate
71 | * Maximum bitrate allowed for broadcast composing
72 | *
73 | * @property {String} streamMode
74 | * The stream mode for the broadcast. This can be set to one of the the following:
75 | *
76 | *
77 | * - "auto" — Streams included in the broadcast are selected automatically
78 | * (the default).
79 | *
80 | * - "manual" — Specify streams to be included based on calls to the
81 | * {@link OpenTok#addBroadcastStream OpenTok.addBroadcastStream()} and
82 | * {@link OpenTok#removeBroadcastStream OpenTok.removeBroadcastStream()} methods.
83 | *
84 | *
85 | * @property {Array} streams
86 | * An array of objects corresponding to streams currently being broadcast.
87 | * This is only set for a broadcast with the status set to "started" and
88 | * the streamMode set to "manual". Each object in the array includes the following properties:
89 | *
90 | * streamId
-- The stream ID of the stream included in the broadcast.
91 | * hasAudio
-- Whether the stream's audio is included in the broadcast.
92 | * hasVideo
-- Whether the stream's video is included in the broadcast.
93 | *
94 | *
95 | * @property {String} multiBroadcastTag
96 | * Set this to support multiple broadcasts for the same session simultaneously. Set this
97 | * to a unique string for each simultaneous broadcast of an ongoing session.
98 | * Note that the multiBroadcastTag
value is not included in the response
99 | * for the methods to list live streaming broadcasts and get information about a live
100 | * streaming broadcast.
101 | *
102 | * @see {@link OpenTok#getBroadcast OpenTok.getBroadcast()}
103 | * @see {@link OpenTok#startBroadcast OpenTok.startBroadcast()}
104 | * @see {@link OpenTok#stopBroadcast OpenTok.stopBroadcast()}
105 | *
106 | * @class Broadcast
107 | */
108 | var Broadcast = function Broadcast(client, json) {
109 | var properties = typeof json === 'string' ? JSON.parse(json) : json;
110 | var hasProp = {}.hasOwnProperty;
111 | var id = properties.id;
112 | var key;
113 |
114 | for (key in properties) {
115 | if (hasProp.call(properties, key) && key !== 'event' && key !== 'partnerId') {
116 | this[key] = properties[key];
117 | }
118 | }
119 |
120 | /**
121 | * Stops the live streaming broadcast.
122 | *
123 | * Broadcasts automatically stop recording after 120 minutes or when all clients have disconnected
124 | * from the session being broadcast.
125 | *
126 | * @param callback {Function} The function to call upon completing the operation. Two arguments
127 | * are passed to the function:
128 | *
129 | *
130 | *
131 | * -
132 | *
error
— An error object (if the call to the method fails).
133 | *
134 | *
135 | * -
136 | *
broadcast
— The Broadcast object.
137 | *
138 | *
139 | *
140 | *
141 | * @method #stop
142 | * @memberof Broadcast
143 | */
144 | this.stop = function (callback) {
145 | client.stopBroadcast(id, function (err, response) {
146 | if (err) {
147 | return callback(new Error('Failed to stop broadcast. ' + err));
148 | }
149 | return callback(null, new Broadcast(client, response));
150 | });
151 | };
152 | };
153 |
154 | module.exports = Broadcast;
155 |
--------------------------------------------------------------------------------
/lib/callbacks.js:
--------------------------------------------------------------------------------
1 | var errors = require('./errors');
2 | const { api } = require('./api');
3 |
4 | function Callback(config, properties) {
5 | var hasProp = {}.hasOwnProperty;
6 | var id = properties.id;
7 | var key;
8 |
9 | for (key in properties) {
10 | if (hasProp.call(properties, key)) this[key] = properties[key];
11 | }
12 |
13 | this.unregister = function (callback) {
14 | exports.unregister(config, id, callback);
15 | };
16 | }
17 |
18 | exports.listCallbacks = function (config, options, callback) {
19 | if (typeof options === 'function') {
20 | callback = options;
21 | options = {};
22 | }
23 |
24 | if (typeof callback !== 'function') {
25 | throw (new errors.ArgumentError('No callback given to listCallbacks'));
26 | }
27 |
28 | const callbackUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/callback`);
29 |
30 | const listCallbackHandler = (err, body, response) => {
31 | if (err) {
32 | callback(err);
33 | }
34 |
35 | callback(null, body.map((item) => new Callback(config, item)));
36 | };
37 |
38 | api({
39 | config: config,
40 | method: 'GET',
41 | url: callbackUrl.toString(),
42 | callback: listCallbackHandler,
43 | });
44 | };
45 |
46 | exports.registerCallback = function (config, options, callback) {
47 | if (typeof options === 'function') {
48 | callback = options;
49 | options = {};
50 | }
51 |
52 | if (typeof callback !== 'function') {
53 | throw (new errors.ArgumentError('No callback given to registerCallback'));
54 | }
55 |
56 | const apiCallback = (err, body, response) => {
57 | if (err) {
58 | callback(err);
59 | return;
60 | }
61 |
62 | callback(null, new Callback(config, body));
63 | };
64 |
65 | const callbackUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/callback`);
66 |
67 | api({
68 | config: config,
69 | method: 'POST',
70 | url: callbackUrl.toString(),
71 | body: {
72 | group: options.group,
73 | event: options.event,
74 | url: options.url
75 | },
76 | callback: apiCallback,
77 | });
78 | };
79 |
80 | exports.unregisterCallback = function (config, callbackId, callback) {
81 | if (typeof callback !== 'function') {
82 | throw (new errors.ArgumentError('No callback given to unregisterCallback'));
83 | }
84 |
85 | if (callbackId == null || callbackId.length === 0) {
86 | callback(new errors.ArgumentError('No callback ID given'));
87 | return;
88 | }
89 |
90 | const callbackUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/callback/${encodeURIComponent(callbackId)}`);
91 |
92 | const callbackHandler = (err) => {
93 | if (err) {
94 | callback(err);
95 | return;
96 | }
97 |
98 | callback(null);
99 | }
100 |
101 | api({
102 | config: config,
103 | method: 'DELETE',
104 | url: callbackUrl.toString(),
105 | callback: callbackHandler,
106 | });
107 | };
108 |
--------------------------------------------------------------------------------
/lib/captions.js:
--------------------------------------------------------------------------------
1 | const errors = require('./errors');
2 | const { api } = require('./api');
3 |
4 | exports.startCaptions = (
5 | config,
6 | sessionId,
7 | token,
8 | {
9 | languageCode = 'en-US',
10 | maxDuration = 14400,
11 | partialCaptions = true
12 | },
13 | callback,
14 | ) => {
15 |
16 | if (typeof callback !== 'function') {
17 | throw new errors.ArgumentError('No callback given to startCaptions');
18 | }
19 |
20 | const captionsUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/captions`);
21 |
22 | const captionsCallback = (err, body, response ) => {
23 | if (response.status === 409) {
24 | callback(new errors.CaptionsError());
25 | return;
26 | }
27 |
28 | if (err) {
29 | callback(err);
30 | return;
31 | }
32 |
33 | const { captionsId } = body
34 | callback(null, captionsId);
35 | }
36 |
37 | api({
38 | config: config,
39 | method: 'POST',
40 | url: captionsUrl.toString(),
41 | body: {
42 | sessionId: sessionId,
43 | token: token,
44 | languageCode: languageCode,
45 | maxDuration: maxDuration,
46 | partialCaptions: partialCaptions,
47 | },
48 | callback: captionsCallback,
49 | });
50 | };
51 |
52 | exports.stopCaptions = (
53 | config,
54 | captionsId,
55 | callback,
56 | ) => {
57 | if (typeof callback !== 'function') {
58 | throw new errors.ArgumentError('No callback given to stopArchive');
59 | }
60 |
61 | const captionsUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/captions/${captionsId}/stop`);
62 | const stopCallback = (err) => {
63 | if (err) {
64 | callback(err);
65 | return;
66 | }
67 |
68 | callback(null, true);
69 | }
70 |
71 | api({
72 | config: config,
73 | method: 'POST',
74 | url: captionsUrl.toString(),
75 | callback: stopCallback,
76 | });
77 | };
78 |
--------------------------------------------------------------------------------
/lib/client.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const Stream = require('./stream');
3 | const Broadcast = require('./broadcast');
4 | const { api } = require('./api');
5 | const defaultConfig = {
6 | apiKey: null,
7 | apiSecret: null,
8 | apiUrl: 'https://api.opentok.com',
9 | endpoints: {
10 | createSession: '/session/create',
11 | getStream:
12 | '/v2/project/<%apiKey%>/session/<%sessionId%>/stream/<%streamId%>',
13 | listStreams: '/v2/project/<%apiKey%>/session/<%sessionId%>/stream',
14 | setArchiveLayout: '/v2/project/<%apiKey%>/archive/<%archiveId%>/layout',
15 | setStreamClassLists: '/v2/project/<%apiKey%>/session/<%sessionId%>/stream',
16 | dial: '/v2/project/<%apiKey%>/dial',
17 | playDTMFToSession: '/v2/project/<%apiKey%>/session/<%sessionId%>/play-dtmf',
18 | playDTMFToClient:
19 | '/v2/project/<%apiKey%>/session/<%sessionId%>/connection/<%connectionId%>/play-dtmf',
20 | forceMuteStream:
21 | '/v2/project/<%apiKey%>/session/<%sessionId%>/stream/<%streamId%>/mute',
22 | forceMute: '/v2/project/<%apiKey%>/session/<%sessionId%>/mute',
23 | startBroadcast: '/v2/project/<%apiKey%>/broadcast',
24 | stopBroadcast: '/v2/project/<%apiKey%>/broadcast/<%broadcastId%>/stop',
25 | getBroadcast: '/v2/project/<%apiKey%>/broadcast/<%broadcastId%>',
26 | patchBroadcast: '/v2/project/<%apiKey%>/broadcast/<%broadcastId%>/streams',
27 | setBroadcastLayout:
28 | '/v2/project/<%apiKey%>/broadcast/<%broadcastId%>/layout',
29 | listBroadcasts: '/v2/project/<%apiKey%>/broadcast',
30 | audioStreamer: '/v2/project/<%apiKey%>/connect'
31 | },
32 | request: {
33 | timeout: 20000 // 20 seconds
34 | },
35 | auth: {
36 | expire: 300
37 | }
38 | };
39 |
40 | const Client = function (c) {
41 | this.c = {};
42 | this.config(_.defaults(c, defaultConfig));
43 | };
44 |
45 | Client.prototype.config = function (c) {
46 | _.merge(this.c, c);
47 | if (this.c.endpoints && this.c.endpoints.dial && this.c.apiKey) {
48 | this.c.endpoints.dial = this.c.endpoints.dial.replace(
49 | /<%apiKey%>/g,
50 | this.c.apiKey
51 | );
52 | }
53 |
54 | return this.c;
55 | };
56 |
57 | Client.prototype.createSession = function (opts, cb) {
58 | const url = new URL(this.c.apiUrl + this.c.endpoints.createSession);
59 |
60 | api({
61 | config:this.c,
62 | url: url.toString(),
63 | method: 'POST',
64 | form: opts,
65 | callback: cb,
66 | });
67 | };
68 |
69 | Client.prototype.startArchive = function () {};
70 |
71 | Client.prototype.stopArchive = function () {};
72 |
73 | Client.prototype.getArchive = function () {};
74 |
75 | Client.prototype.listArchives = function () {};
76 |
77 | Client.prototype.deleteArchive = function () {};
78 |
79 | Client.prototype.playDTMF = function (opts, cb) {
80 | let url;
81 |
82 | if (opts.sessionId) {
83 | url = this.c.apiUrl
84 | + this.c.endpoints.playDTMFToSession
85 | .replace(/<%apiKey%>/g, this.c.apiKey)
86 | .replace(/<%sessionId%>/g, opts.sessionId);
87 | }
88 | if (opts.connectionId) {
89 | url = this.c.apiUrl
90 | + this.c.endpoints.playDTMFToClient
91 | .replace(/<%apiKey%>/g, this.c.apiKey)
92 | .replace(/<%sessionId%>/g, opts.sessionId)
93 | .replace(/<%connectionId%>/g, opts.connectionId);
94 | }
95 |
96 | api({
97 | config:this.c,
98 | url:url,
99 | method: 'POST',
100 | body: {
101 | digits: opts.digits
102 | },
103 | callback: cb,
104 | });
105 | };
106 |
107 | Client.prototype.forceMuteStream = function (opts, cb) {
108 | const url = this.c.apiUrl
109 | + this.c.endpoints.forceMuteStream
110 | .replace(/<%apiKey%>/g, this.c.apiKey)
111 | .replace(/<%sessionId%>/g, opts.sessionId)
112 | .replace(/<%streamId%>/g, opts.streamId);
113 |
114 | api({
115 | config:this.c,
116 | url:url,
117 | method: 'POST',
118 | body: { },
119 | callback: cb,
120 | });
121 | };
122 |
123 | Client.prototype.forceMuteAll = function (opts, cb) {
124 | const url = this.c.apiUrl
125 | + this.c.endpoints.forceMute
126 | .replace(/<%apiKey%>/g, this.c.apiKey)
127 | .replace(/<%sessionId%>/g, opts.sessionId);
128 |
129 | opts.options.active = true;
130 |
131 | api({
132 | config:this.c,
133 | url:url,
134 | method: 'POST',
135 | body: opts.options,
136 | callback: cb,
137 | });
138 | };
139 |
140 | Client.prototype.disableForceMute = function (opts, cb) {
141 | const url = this.c.apiUrl
142 | + this.c.endpoints.forceMute
143 | .replace(/<%apiKey%>/g, this.c.apiKey)
144 | .replace(/<%sessionId%>/g, opts.sessionId);
145 |
146 | const options = {
147 | active: false
148 | };
149 |
150 | api({
151 | config:this.c,
152 | url:url,
153 | method: 'POST',
154 | body: options,
155 | callback: cb,
156 | });
157 | };
158 |
159 | Client.prototype.setArchiveLayout = function setArchiveLayout(opts, cb) {
160 | const url = this.c.apiUrl
161 | + this.c.endpoints.setArchiveLayout
162 | .replace(/<%apiKey%>/g, this.c.apiKey)
163 | .replace(/<%archiveId%>/g, opts.archiveId);
164 | api({
165 | config:this.c,
166 | url:url,
167 | method: 'PUT',
168 | body: {
169 | type: opts.type,
170 | stylesheet: opts.stylesheet || undefined,
171 | screenshareType: opts.screenshareType || undefined
172 | },
173 | callback: cb,
174 | });
175 | };
176 |
177 | Client.prototype.startBroadcast = function (opts, cb) {
178 | const url = this.c.apiUrl
179 | + this.c.endpoints.startBroadcast.replace(/<%apiKey%>/g, this.c.apiKey);
180 |
181 | api({
182 | config:this.c,
183 | url:url,
184 | method: 'POST',
185 | body: opts,
186 | callback: (err, body, response) => {
187 | if (response.status === 400) {
188 | cb(new Error('Bad session ID, token, SIP credentials, or SIP URI (sip:user@domain.tld)'));
189 | return;
190 | }
191 |
192 | cb(err, body);
193 | },
194 | });
195 |
196 | };
197 |
198 | Client.prototype.patchBroadcast = function patchBroadcast(broadcastId, opts, cb) {
199 | const url = this.c.apiUrl + this.c.endpoints.patchBroadcast.replace(/<%apiKey%>/g, this.c.apiKey)
200 | .replace(/<%broadcastId%>/g, broadcastId);
201 | api({
202 | config:this.c,
203 | url:url,
204 | method: 'PATCH',
205 | body: opts,
206 | callback: cb,
207 | });
208 |
209 | };
210 |
211 | Client.prototype.stopBroadcast = function (broadcastId, cb) {
212 | const url = this.c.apiUrl
213 | + this.c.endpoints.stopBroadcast
214 | .replace(/<%apiKey%>/g, this.c.apiKey)
215 | .replace(/<%broadcastId%>/g, broadcastId);
216 | api({
217 | config:this.c,
218 | url:url,
219 | method: 'POST',
220 | body: { },
221 | callback: (err, json) => {
222 | const responseText = typeof json === 'object' ? JSON.stringify(json) : json;
223 | cb(err, responseText);
224 | },
225 | });
226 | };
227 |
228 | Client.prototype.getBroadcast = function getBroadcast(broadcastId, cb) {
229 | const url = this.c.apiUrl
230 | + this.c.endpoints.getBroadcast
231 | .replace(/<%apiKey%>/g, this.c.apiKey)
232 | .replace(/<%broadcastId%>/g, broadcastId);
233 | api({
234 | config:this.c,
235 | url:url,
236 | method: 'GET',
237 | callback: cb,
238 | });
239 | };
240 |
241 | Client.prototype.listBroadcasts = function listBroadcasts(queryString, cb) {
242 | const baseUrl = this.c.apiUrl
243 | + this.c.endpoints.listBroadcasts.replace(/<%apiKey%>/g, this.c.apiKey);
244 | const url = queryString.length > 0 ? baseUrl + '?' + queryString : baseUrl;
245 | api({
246 | config:this.c,
247 | url:url,
248 | method: 'GET',
249 | callback: (err, items) => {
250 | cb(
251 | err,
252 | items?.items.map((item) => new Broadcast(Client, JSON.stringify(item))),
253 | items?.count,
254 | )
255 | },
256 | });
257 | };
258 |
259 | Client.prototype.setBroadcastLayout = function setBroadcastLayout(opts, cb) {
260 | const url = this.c.apiUrl
261 | + this.c.endpoints.setBroadcastLayout
262 | .replace(/<%apiKey%>/g, this.c.apiKey)
263 | .replace(/<%broadcastId%>/g, opts.broadcastId);
264 | api({
265 | config:this.c,
266 | url:url,
267 | method: 'PUT',
268 | body: {
269 | type: opts.type,
270 | stylesheet: opts.stylesheet || undefined,
271 | screenshareType: opts.screenshareType || undefined
272 | },
273 | callback: cb,
274 | });
275 |
276 | };
277 |
278 | Client.prototype.websocketConnect = function websocketConnect(opts, cb) {
279 | const url = this.c.apiUrl + this.c.endpoints.audioStreamer
280 | .replace(/<%apiKey%>/g, this.c.apiKey);
281 | api({
282 | config:this.c,
283 | url:url,
284 | method: 'POST',
285 | body: opts,
286 | callback: cb,
287 | });
288 |
289 | };
290 |
291 | Client.prototype.setStreamClassLists = function setStreamClassLists(
292 | sessionId,
293 | classListArray,
294 | cb
295 | ) {
296 | const url = this.c.apiUrl
297 | + this.c.endpoints.setStreamClassLists
298 | .replace(/<%apiKey%>/, this.c.apiKey)
299 | .replace(/<%sessionId%>/g, sessionId);
300 | api({
301 | config:this.c,
302 | url:url,
303 | method: 'PUT',
304 | body: {
305 | items: classListArray
306 | },
307 | callback: cb,
308 | });
309 | };
310 |
311 | Client.prototype.dial = function (opts, cb) {
312 | api({
313 | config:this.c,
314 | url: this.c.apiUrl + this.c.endpoints.dial,
315 | method: 'POST',
316 | body: opts,
317 | callback: cb,
318 | });
319 | };
320 |
321 | Client.prototype.getStream = function getStream(sessionId, streamId, cb) {
322 | const url = this.c.apiUrl
323 | + this.c.endpoints.getStream
324 | .replace(/<%apiKey%>/g, this.c.apiKey)
325 | .replace(/<%streamId%>/g, streamId)
326 | .replace(/<%sessionId%>/g, sessionId);
327 | api({
328 | config:this.c,
329 | url: url,
330 | method: 'GET',
331 | callback: cb,
332 | });
333 |
334 | };
335 |
336 | Client.prototype.listStreams = function listStreams(sessionId, cb) {
337 | const url = this.c.apiUrl
338 | + this.c.endpoints.listStreams
339 | .replace(/<%apiKey%>/g, this.c.apiKey)
340 | .replace(/<%sessionId%>/g, sessionId);
341 | api({
342 | config:this.c,
343 | url: url,
344 | method: 'GET',
345 | callback: (err, body) => {
346 | cb(err, body?.items?.map((stream) => new Stream(JSON.stringify(stream))) || [])
347 | },
348 | });
349 | };
350 |
351 | module.exports = Client;
352 |
--------------------------------------------------------------------------------
/lib/errors.js:
--------------------------------------------------------------------------------
1 |
2 | exports.ArgumentError = function (message) {
3 | this.message = message;
4 | };
5 |
6 | exports.ArgumentError.prototype = Object.create(Error.prototype);
7 |
8 | exports.AuthError = function (message = 'Invalid API key or secret') {
9 | this.message = message;
10 | };
11 |
12 | exports.AuthError.prototype = Object.create(Error.prototype);
13 |
14 |
15 | exports.ArchiveError = function (message) {
16 | this.message = message;
17 | };
18 |
19 | exports.ArchiveError.prototype = Object.create(Error.prototype);
20 |
21 | exports.CaptionsError = function (message = 'Live captions have already started for this OpenTok Session') {
22 | this.message = message;
23 | };
24 |
25 | exports.SipError = function (message) {
26 | this.message = message;
27 | };
28 |
29 | exports.SipError.prototype = Object.create(Error.prototype);
30 |
31 |
32 | exports.SignalError = function (message) {
33 | this.message = message;
34 | };
35 |
36 | exports.SignalError.prototype = Object.create(Error.prototype);
37 |
38 |
39 | exports.ForceDisconnectError = function (message) {
40 | this.message = message;
41 | };
42 |
43 | exports.ForceDisconnectError.prototype = Object.create(Error.prototype);
44 |
45 |
46 | exports.CallbackError = function (message) {
47 | this.message = message;
48 | };
49 |
50 | exports.CallbackError.prototype = Object.create(Error.prototype);
51 |
52 | exports.RequestError = function (message = 'Unexpected response from OpenTok') {
53 | this.message = message;
54 | };
55 |
56 | exports.RequestError.prototype = Object.create(Error.prototype);
57 |
58 | exports.NotFoundError = function (message = 'Not Found') {
59 | this.message = message;
60 | };
61 |
62 | exports.NotFoundError.prototype = Object.create(Error.prototype);
63 |
--------------------------------------------------------------------------------
/lib/generateJwt.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const { v4 } = require('uuid')
3 | const debug = require('debug');
4 |
5 | const log = debug('@opentok');
6 |
7 | module.exports = function (config, additionalClaims) {
8 | const currentTime = Math.floor(new Date() / 1000);
9 | const initialClaims = {
10 | iss: config.apiKey,
11 | ist: 'project',
12 | iat: currentTime,
13 | exp: additionalClaims?.expire_time || currentTime + config.auth.expire,
14 | };
15 |
16 | const vonageClaims = {
17 | application_id: config.apiKey,
18 | jti: v4(),
19 | }
20 |
21 | const claims = {
22 | ...initialClaims,
23 | ...additionalClaims,
24 | ...(config.callVonage ? vonageClaims : {}),
25 | };
26 |
27 | log('JWT Claims', claims);
28 |
29 | const token = jwt.sign(
30 | claims,
31 | config.apiSecret,
32 | config.callVonage
33 | ? {
34 | algorithm: 'RS256',
35 | header: {
36 | typ: 'JWT',
37 | alg: 'RS256'
38 | },
39 | }
40 | : {},
41 | );
42 |
43 | return token;
44 | };
45 |
--------------------------------------------------------------------------------
/lib/moderation.js:
--------------------------------------------------------------------------------
1 | var errors = require('./errors');
2 | const { api } = require('./api');
3 |
4 | exports.forceDisconnect = function (config, sessionId, connectionId, callback) {
5 | if (typeof callback !== 'function') {
6 | throw (new errors.ArgumentError('No callback given to forceDisconnect'));
7 | }
8 |
9 | if (sessionId == null || connectionId == null) {
10 | return callback(new errors.ArgumentError('No sessionId or connectionId given to forceDisconnect'));
11 | }
12 |
13 | const moderationUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/session/${sessionId}`);
14 |
15 | if (connectionId) {
16 | moderationUrl.pathname = `${moderationUrl.pathname}/connection/${connectionId}`
17 | }
18 |
19 | const moderationHandler = (err, body, response) => {
20 | if (response.status === 404) {
21 | callback(new errors.ForceDisconnectError('Session or Connection not found'));
22 | return;
23 | }
24 |
25 | callback(err);
26 | };
27 |
28 | api({
29 | config: config,
30 | method: 'DELETE',
31 | url: moderationUrl.toString(),
32 | callback: moderationHandler,
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/lib/render.js:
--------------------------------------------------------------------------------
1 | const errors = require('./errors');
2 | const { api } = require('./api');
3 | const _ = require('lodash');
4 |
5 | const guardParams = (method, options, callback) => {
6 | const cb = typeof options === 'function' ? options : callback;
7 | const opts = typeof options !== 'function' ? options : { count: 50 };
8 |
9 | if (typeof cb !== 'function') {
10 | throw new errors.ArgumentError('No callback given to ' + method);
11 | }
12 |
13 | return [cb, opts];
14 | };
15 |
16 | /**
17 | * An object representing an Experience Composer renderer.
18 | *
19 | * @property {String} id
20 | * The ID of the render instance
21 | *
22 | * @property {String} projectId
23 | * The ID of the project for the render.
24 | *
25 | * @property {String} sessionId
26 | * The ID of the session being rendered into.
27 | *
28 | * @property {Number} createdAt
29 | * The time at which the render was created, in milliseconds since the UNIX epoch.
30 | *
31 | * @property {Number} upddatedAt
32 | * The time at which the render was created, in milliseconds since the UNIX epoch.
33 | *
34 | * @property {String} url
35 | * A publically reachable URL controlled by the customer and capable of generating
36 | * the content to be rendered without user intervention.
37 | *
38 | * @proerpty {String} status
39 | * Current status of for the render. Will be one of `starting`, `started`, `stopped`
40 | * or `failed`
41 | *
42 | * @property {String} reason
43 | * Gives a short textual reason for why the Render failed or stopped.
44 | *
45 | * @property {String} callbackUrl
46 | * URL of the customer service where the callbacks will be received.
47 | *
48 | * @property {String} event
49 | * Last sent event of for the render
50 | *
51 | * @property {String} resoultion
52 | * Resolution of the display area for the composition.
53 | *
54 | * @see {@link OpenTok#getRender OpenTok.getRender()}
55 | * @see {@link OpenTok#startRender OpenTok.startRender()}
56 | * @see {@link OpenTok#stopRender OpenTok.stopRender()}
57 | * @see {@link OpenTok#listRenders OpenTok.listRenders()}
58 | *
59 | * @class Render
60 | */
61 |
62 |
63 | const handleResponse = (callback) => (err, body, response) => {
64 | if (err) {
65 | callback(err);
66 | return;
67 | }
68 |
69 | callback(null, body, response);
70 | };
71 |
72 | /**
73 | * Return a list of {@link Render} objects, representing Experience Composer in any status
74 | *
75 | * @param {Object} config - API configuration settings {@see OpenTok.listRenders}
76 | * @param {Object} options - Optional parameters for the API call {@see OpenTok.listRenders}
77 | * @param {Function} callback - Callback function
78 | *
79 | * @method #listRenders
80 | * @memberof Render
81 | */
82 | exports.listRenders = (config, options, callback) => {
83 | if (typeof callback !== 'function') {
84 | throw new errors.ArgumentError('No callback given to listRenders');
85 | }
86 |
87 | const { offset, count } = options;
88 |
89 | if (count > 1000 || count < 1) {
90 | throw new errors.ArgumentError('Count is out of range');
91 | }
92 |
93 | const renderUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/render`);
94 |
95 | if (offset) {
96 | renderUrl.searchParams.set('offset', offset);
97 | }
98 |
99 | if (count) {
100 | renderUrl.searchParams.set('count', count);
101 | }
102 |
103 | api({
104 | config: config,
105 | url: renderUrl.toString(),
106 | callback: handleResponse(callback),
107 | });
108 | };
109 |
110 | /**
111 | * Gets a {@link Render} object for the given render ID.
112 | *
113 | * @param {Object} config - API config {@see OpenTok}
114 | * @param {String} renderId - The Render ID to fetch
115 | * @param {Function} callback - Callback function
116 | *
117 | * @method #getRender
118 | * @memberof Render
119 | */
120 | exports.getRender = (config, renderId, callback) => {
121 | if (typeof callback !== 'function') {
122 | throw new errors.ArgumentError('No callback given to getRender');
123 | }
124 |
125 | const renderUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/render/${renderId}`);
126 |
127 | api({
128 | method: 'GET',
129 | config: config,
130 | url: renderUrl.toString(),
131 | callback: handleResponse(callback)
132 | });
133 | };
134 |
135 | /**
136 | * Starts an Experience Composer for an OpenTok session.
137 | *
138 | *
139 | * @param {Object} config - API configuration settings {@see OpenTok.startRender}
140 | * @param {Object} options - Optional parameters for the API call {@see OpenTok.startRender}
141 | * @param {Function} callback - Callback function
142 | *
143 | * @method #startRender
144 | * @memberof Render
145 | */
146 | exports.startRender = (config, options, callback) => {
147 | if (typeof callback !== 'function') {
148 | throw new errors.ArgumentError('No callback given to startRender');
149 | }
150 |
151 | const [cb, opts] = guardParams('startRender', options, callback);
152 |
153 | const renderUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/render`);
154 |
155 | api({
156 | url: renderUrl.toString(),
157 | config: config,
158 | method: 'POST',
159 | body: {
160 | sessionId: _.get(opts, 'sessionId'),
161 | token: _.get(opts, 'token'),
162 | url: _.get(opts, 'url'),
163 | maxDuration: _.get(opts, 'maxDuration', 1800),
164 | resolution: _.get(opts, 'resolution', '1280x720'),
165 | statusCallbackUrl: _.get(opts, 'statusCallbackUrl')
166 | },
167 | callback: handleResponse(cb),
168 | });
169 | };
170 |
171 | /**
172 | * Stops an OpenTok render that is being rendered.
173 | *
174 | * @param {Object} config - API configuration settings {@see OpenTok.stopRender}
175 | * @param {String} renderId - The Render ID to fetch
176 | * @param {Function} callback - Callback function
177 | *
178 | * @method #stopRender
179 | * @memberof Render
180 | */
181 | exports.stopRender = (config, renderId, callback) => {
182 | if (typeof callback !== 'function') {
183 | throw new errors.ArgumentError('No callback given to stopRender');
184 | }
185 |
186 | const renderUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/render/${renderId}`);
187 |
188 | api({
189 | method: 'DELETE',
190 | config: config,
191 | url: renderUrl.toString(),
192 | callback: handleResponse(callback),
193 | });
194 | };
195 |
--------------------------------------------------------------------------------
/lib/session.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Represents an OpenTok session. Use the {@link OpenTok#createSession OpenTok.createSession()}
3 | * method to create an OpenTok session. The sessionId
property of the Session object
4 | * is the session ID.
5 | *
6 | * @property {String} sessionId The session ID.
7 | *
8 | * @class Session
9 | */
10 |
11 | var Session = function Session(ot, sessionId, properties) {
12 | var prop;
13 | this.ot = ot;
14 | this.sessionId = sessionId;
15 | for (prop in properties) {
16 | if ({}.hasOwnProperty.call(properties, prop)) {
17 | this[prop] = properties[prop];
18 | }
19 | }
20 | };
21 |
22 | Session.prototype.generateToken = function generateToken(opts) {
23 | return this.ot.generateToken(this.sessionId, opts);
24 | };
25 |
26 | module.exports = Session;
27 |
--------------------------------------------------------------------------------
/lib/signaling.js:
--------------------------------------------------------------------------------
1 | var errors = require('./errors');
2 | const {api} = require('./api');
3 |
4 | exports.signal = function (config, sessionId, connectionId, payload, callback) {
5 | if (typeof callback !== 'function') {
6 | throw (new errors.ArgumentError('No callback given to signal'));
7 | }
8 |
9 | if (sessionId == null || payload == null) {
10 | return callback(new errors.ArgumentError('No sessionId or payload given to signal'));
11 | }
12 |
13 | const signalUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/session/${sessionId}`);
14 | if (connectionId) {
15 | signalUrl.pathname = `${signalUrl.pathname}/connection/${connectionId}`;
16 | }
17 |
18 | signalUrl.pathname = `${signalUrl.pathname}/signal`;
19 |
20 | const signalHandler = (err, body, response) => {
21 | if (response.status === 404) {
22 | callback(new errors.SignalError('Session or Connection not found'));
23 | return;
24 | }
25 |
26 | callback(err);
27 | };
28 |
29 | return api({
30 | config: config,
31 | method: 'POST',
32 | url: signalUrl.toString(),
33 | body: payload,
34 | callback: signalHandler,
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/lib/sipInterconnect.js:
--------------------------------------------------------------------------------
1 | /**
2 | * An object representing an OpenTok SIP call.
3 | *
4 | * Do not call the new()
constructor. To start a SIP call, call the
5 | * {@link OpenTok#dial OpenTok.dial()} method.
6 | *
7 | * @property {String} id
8 | * The unique ID of the SIP conference.
9 | *
10 | * @property {String} connectionId
11 | * The connection ID of the audio-only stream that is put into an OpenTok Session.
12 | *
13 | * @property {String} streamId
14 | * The stream ID of the audio-only stream that is put into an OpenTok Session.
15 | *
16 | *
17 | * @see {@link OpenTok#dial OpenTok.dial()}
18 | *
19 | * @class SipInterconnect
20 | */
21 | function SipInterconnect(config, properties) {
22 | var hasProp = {}.hasOwnProperty;
23 | var key;
24 |
25 | for (key in properties) {
26 | if (hasProp.call(properties, key)) {
27 | this[key] = properties[key];
28 | }
29 | }
30 | }
31 |
32 | module.exports = SipInterconnect;
33 |
--------------------------------------------------------------------------------
/lib/stream.js:
--------------------------------------------------------------------------------
1 | /**
2 | * An object representing an OpenTok stream. This is passed into the callback function
3 | * of the {@link OpenTok#getStream OpenTok.getStream()} method.
4 | *
5 | * @property {String} id
6 | * The stream ID of the stream.
7 | *
8 | * @property {String} name
9 | * The stream name (if one was set when the client published the stream).
10 | *
11 | * @property {Array} layoutClassList
12 | * An array of the layout classes for the stream. These layout classes are used in
13 | * customizing the layout in
14 | * Live
15 | * streaming broadcasts and
16 | *
17 | * composed archives.
18 | *
19 | * @property {String} videoType
20 | * Set to either "camera" or "screen". A "screen" video uses screen sharing on the publisher as
21 | * the video source; for other videos, this property is set to "camera".
22 | *
23 | * @see {@link OpenTok#dial OpenTok.dial()}
24 | *
25 | * @class Stream
26 | */
27 | function Stream(json) {
28 | var properties = JSON.parse(json);
29 | var hasProp = {}.hasOwnProperty;
30 | var key;
31 |
32 | for (key in properties) {
33 | if (hasProp.call(properties, key)) {
34 | this[key] = properties[key];
35 | }
36 | }
37 | }
38 |
39 | module.exports = Stream;
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/package.json",
3 | "name": "opentok",
4 | "version": "2.21.2",
5 | "description": "OpenTok server-side SDK",
6 | "homepage": "https://github.com/opentok/opentok-node",
7 | "bugs": {
8 | "url": "https://github.com/opentok/opentok-node/issues",
9 | "email": "support@tokbox.com"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/opentok/opentok-node.git"
14 | },
15 | "license": "MIT",
16 | "contributors": [
17 | {
18 | "name": "Hashir Baqai",
19 | "email": "hashirbaqai@gmail.com"
20 | },
21 | {
22 | "name": "Brian Stoner",
23 | "email": "bsstoner@gmail.com",
24 | "url": "http://brianstoner.com"
25 | },
26 | {
27 | "name": "Song Zheng",
28 | "email": "song@tokbox.com",
29 | "url": "http://songz.me"
30 | },
31 | {
32 | "name": "Ankur Oberoi",
33 | "email": "aoberoi@gmail.com",
34 | "url": "http://aoberoi.me"
35 | },
36 | {
37 | "name": "Jeff Swartz",
38 | "email": "swartz@tokbox.com"
39 | },
40 | {
41 | "name": "Hamza Nasir",
42 | "email": "mnasir@hawk.iit.edu"
43 | },
44 | {
45 | "name": "Manik Sachdeva",
46 | "url": "http://maniksach.dev"
47 | },
48 | {
49 | "name": "Michael Jolley",
50 | "url": "https://baldbeardedbuilder.com"
51 | },
52 | {
53 | "name": "Alex Lakatos",
54 | "url": "twitter.com/lakatos88"
55 | },
56 | {
57 | "name": "Mofi Rahman",
58 | "url": "https://twitter.com/moficodes"
59 | },
60 | {
61 | "name": "Chuck \"MANCHUCK\" Reeves",
62 | "email": "chuck@manchuck.com",
63 | "url": "https://github.com/manchuck"
64 | }
65 | ],
66 | "main": "lib/opentok.js",
67 | "files": [
68 | "lib/"
69 | ],
70 | "scripts": {
71 | "lint": "eslint ./lib/ ./test/ ./sample/",
72 | "lint-fix": "eslint --fix",
73 | "mocha-coverage": "cross-env NODE_ENV=test nyc --reporter=text-lcov mocha > mocha.lcov",
74 | "report-coverage": "npm run mocha-coverage",
75 | "test": "npm run test-no-lint",
76 | "test-coverage": "cross-env NODE_ENV=test nyc mocha",
77 | "test-coverage-html": "cross-env NODE_ENV=test nyc --reporter html mocha",
78 | "test-no-lint": "mocha ./test/*-test.js"
79 | },
80 | "dependencies": {
81 | "@vonage/jwt": "1.11.0",
82 | "debug": "4.4.0",
83 | "jsonwebtoken": "9.0.2",
84 | "lodash": "4.17.21",
85 | "node-fetch": "2.7.0",
86 | "opentok-token": "1.1.1",
87 | "uuid": "11.0.5"
88 | },
89 | "devDependencies": {
90 | "chai": "4.3.10",
91 | "cross-env": "7.0.3",
92 | "eslint": "8.51.0",
93 | "eslint-config-airbnb-base": "15.0.0",
94 | "eslint-plugin-import": "2.28.1",
95 | "mocha": "10.8.2",
96 | "nock": "13.3.6",
97 | "nyc": "15.1.0"
98 | },
99 | "engines": {
100 | "node": ">=4"
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/sample/Archiving/README.md:
--------------------------------------------------------------------------------
1 | # OpenTok Archiving Sample for Node
2 |
3 | This is a simple demo app that shows how you can use the OpenTok Node SDK to archive (or record)
4 | Sessions, list archives that have been created, download the recordings, and delete the recordings.
5 |
6 | ## Running the App
7 |
8 | First, download the dependencies using [npm](https://www.npmjs.org) in this directory.
9 |
10 | ```
11 | $ npm install
12 | ```
13 |
14 | Next, add your own API Key and API Secret to the environment variables. There are a few ways to do
15 | this but the simplest would be to do it right in your shell.
16 |
17 | ```
18 | $ export API_KEY=0000000
19 | $ export API_SECRET=abcdef1234567890abcdef01234567890abcdef
20 | ```
21 |
22 | Finally, start the app using node
23 |
24 | ```
25 | $ node index.js
26 | ```
27 |
28 | Visit in your browser. You can now create new archives (either as a host or
29 | as a participant) and also play archives that have already been created.
30 |
31 | ## Walkthrough
32 |
33 | This demo application uses the same frameworks and libraries as the HelloWorld sample. If you have
34 | not already gotten familiar with the code in that project, consider doing so before continuing.
35 |
36 | The explanations below are separated by page. Each section will focus on a route handler within the
37 | main application (index.js).
38 |
39 | ### Creating Archives – Host View
40 |
41 | Start by visiting the host page at and using the application to record
42 | an archive. Your browser will first ask you to approve permission to use the camera and microphone.
43 | Once you've accepted, your image will appear inside the section titled 'Host'. To start recording
44 | the video stream, press the 'Start Archiving' button. Once archiving has begun the button will turn
45 | green and change to 'Stop Archiving'. You should also see a red blinking indicator that you are
46 | being recorded. Wave and say hello! Stop archiving when you are done.
47 |
48 | Next we will see how the host view is implemented on the server. The route handler for this page is
49 | shown below:
50 |
51 | ```javascript
52 | app.get('/host', function(req, res) {
53 | var sessionId = app.get('sessionId'),
54 | // generate a fresh token for this client
55 | token = opentok.generateToken(sessionId, { role: 'moderator' });
56 |
57 | res.render('host.ejs', {
58 | apiKey: apiKey,
59 | sessionId: sessionId,
60 | token: token
61 | });
62 | });
63 | ```
64 |
65 | If you've completed the HelloWorld walkthrough, this should look familiar. This handler simply
66 | generates the three strings that the client (JavaScript) needs to connect to the session: `apiKey`,
67 | `sessionId` and `token`. After the user has connected to the session, they press the
68 | 'Start Archiving' button, which sends an XHR (or Ajax) request to the
69 | URL. The route handler for this URL is shown below:
70 |
71 | ```javascript
72 | app.post('/start', function(req, res) {
73 | var hasAudio = (req.param('hasAudio') !== undefined);
74 | var hasVideo = (req.param('hasVideo') !== undefined);
75 | var outputMode = req.param('outputMode');
76 | var archiveOptions = {
77 | name: 'Node Archiving Sample App',
78 | hasAudio: hasAudio,
79 | hasVideo: hasVideo,
80 | outputMode: outputMode,
81 | };
82 | if (outputMode === 'composed') {
83 | startOptions.layout = { type: 'horizontalPresentation' };
84 | }
85 | opentok.startArchive(app.get('sessionId'), archiveOptions, function(err, archive) {
86 | if (err) return res.send(500,
87 | 'Could not start archive for session '+sessionId+'. error='+err.message
88 | );
89 | res.json(archive);
90 | });
91 | });
92 | ```
93 |
94 | In this handler, the `startArchive()` method of the `opentok` instance is called with the
95 | `sessionId` for the session that needs to be archived. In this case, as in the HelloWorld
96 | sample app, there is only one session created and it is used here and for the participant view.
97 | This will trigger the recording to begin.
98 |
99 | The optional second argument is for options. The `name` is stored with the archive and can
100 | be read later. The `hasAudio`, `hasVideo`, `outputMode`, values are read from the request body;
101 | these define whether the archive will record audio and video, and whether it will record streams
102 | individually or to a single file composed of all streams. See the "Changing Archive Layout" section
103 | below for information on the `layout` option.
104 |
105 | The last argument is the callback for the result of this asynchronous function.
106 | The callback signature follows the common node pattern of using the first argument fo
107 | an error if one occurred, otherwise the second parameter is an Archive object. As long
108 | as there is no error, a response is sent back to the client's XHR request with the JSON
109 | representation of the archive. The client is also listening for the `archiveStarted` event, and uses
110 | that event to change the 'Start Archiving' button to show 'Stop Archiving' instead. When the user
111 | presses the button this time, another XHR request is sent to the
112 | URL where `:archiveId` represents the ID the client receives
113 | in the 'archiveStarted' event. The route handler for this request is shown below:
114 |
115 | ```javascript
116 | app.get('/stop/:archiveId', function(req, res) {
117 | var archiveId = req.param('archiveId');
118 | opentok.stopArchive(archiveId, function(err, archive) {
119 | if (err) return res.send(500, 'Could not stop archive '+archiveId+'. error='+err.message);
120 | res.json(archive);
121 | });
122 | });
123 | ```
124 |
125 | This handler is very similar to the previous one. Instead of calling the `startArchive()` method,
126 | the `stopArchive()` method is called. This method takes an `archiveId` as its parameter, which
127 | is different for each time a session starts recording. But the client has sent this to the server
128 | as part of the URL, so the `req.param('archiveId')` expression is used to retrieve it.
129 |
130 | Now you have understood the three main routes that are used to create the Host experience of
131 | creating an archive. Much of the functionality is done in the client with JavaScript. That code can
132 | be found in the `public/js/host.js` file. Read about the
133 | [OpenTok.js JavaScript](http://tokbox.com/opentok/libraries/client/js/) library to learn more.
134 |
135 | ### Creating Archives - Participant View
136 |
137 | With the host view still open and publishing, open an additional window or tab and navigate to
138 | and allow the browser to use your camera and microphone. Once
139 | again, start archiving in the host view. Back in the participant view, notice that the red blinking
140 | indicator has been shown so that the participant knows his video is being recorded. Now stop the
141 | archiving in the host view. Notice that the indicator has gone away in the participant view too.
142 |
143 | Creating this view on the server is as simple as the HelloWorld sample application. See the code
144 | for the route handler below:
145 |
146 | ```javascript
147 | app.get('/participant', function(req, res) {
148 | var sessionId = app.get('sessionId'),
149 | // generate a fresh token for this client
150 | token = opentok.generateToken(sessionId, { role: 'moderator' });
151 |
152 | res.render('participant.ejs', {
153 | apiKey: apiKey,
154 | sessionId: sessionId,
155 | token: token
156 | });
157 | });
158 | ```
159 |
160 | Since this view has no further interactivity with buttons, this is all that is needed for a client
161 | that is participating in an archived session. Once again, much of the functionality is implemented
162 | in the client, in code that can be found in the `public/js/participant.js` file.
163 |
164 | ### Changing Archive Layout
165 |
166 | *Note:* Changing archive layout is only available for composed archives, and setting the layout
167 | is not required. By default, composed archives use the "best fit" layout. For more information,
168 | see the OpenTok developer guide for [Customizing the video layout for composed
169 | archives](https://tokbox.com/developer/guides/archiving/layout-control.html).
170 |
171 | When you create a composed archive (when the `outputMode` is set to 'composed), we set the
172 | `layout` property of the `options` object passed into `OpenTok.startArchive()` to
173 | `'horizontalPresentation'`. This sets the initial layout type of the archive.
174 | `'horizontalPresentation'` is one of the predefined layout types for composed archives.
175 |
176 | For composed archives, you can change the layout dynamically. The host view includes a
177 | *Toggle layout* button. This toggles the layout of the streams between a horizontal and vertical
178 | presentation. When you click this button, the host client switches makes an HTTP POST request to
179 | the '/archive/:archiveId/layout' endpoint:
180 |
181 | ```javascript
182 | app.post('/archive/:archiveId/layout', function (req, res) {
183 | var archiveId = req.param('archiveId');
184 | var type = req.body.type;
185 | app.set('layout', type);
186 | opentok.setArchiveLayout(archiveId, type, null, function (err) {
187 | if (err) return res.send(500, 'Could not set layout ' + type + '. error=' + err.message);
188 | res.send(200, 'OK');
189 | });
190 | });
191 | ```
192 |
193 | This calls the `OpenTok.setArchiveLayout()` method of the OpenTok Node.js SDK, setting the
194 | archive layout to the layout type defined in the POST request's body. The layout type will
195 | either be set to `horizontalPresentation` or `verticalPresentation`, which are two of the predefined layout types for OpenTok composed archives.
196 |
197 | Also, in the host view, you can click any stream to set it to be the focus stream in the
198 | archive layout. (Click outside of the mute audio icon.) Doing so sends an HTTP POST request
199 | to the `/focus` endpoint:
200 |
201 | ```javascript
202 | app.post('/focus', function (req, res) {
203 | var otherStreams = req.body.otherStreams;
204 | var focusStreamId = req.body.focus;
205 | var classListArray = [];
206 | if (otherStreams) {
207 | var i;
208 | for (i = 0; i < otherStreams.length; i++) {
209 | classListArray.push({
210 | id: otherStreams[i],
211 | layoutClassList: [],
212 | });
213 | }
214 | }
215 | classListArray.push({
216 | id: focusStreamId,
217 | layoutClassList: ['focus'],
218 | });
219 | app.set('focusStreamId', focusStreamId);
220 | opentok.setStreamClassLists(app.get('sessionId'), classListArray, function (err) {
221 | if (err) return res.send(500, 'Could not set class lists. Error:' + err.message);
222 | return res.send(200, 'OK');
223 | });
224 | });
225 | ```
226 |
227 | The body of the POST request includes the stream ID of the "focus" stream and an array of
228 | other stream IDs in the session. The server-side method that handles the POST requests assembles
229 | a `classListArray` array, based on these stream IDs:
230 |
231 | ```javascript
232 | [
233 | {
234 | "id": "6ad90229-df4f-4849-8974-5d675727c8b5",
235 | "layoutClassList": []
236 | },
237 | {
238 | "id": "aef616a5-769c-43e9-96d2-221edb986cbf",
239 | "layoutClassList": []
240 | },
241 | {
242 | "id": "db9f2372-7564-4b38-9bb2-d6ba4249fe63",
243 | "layoutClassList": ["focus"]
244 | }
245 | ]
246 | ```
247 |
248 | This is passed in as the `classListArray` parameter of the `OpenTok.setStreamClassLists()` method
249 | of the OpenTok Node.js SDK:
250 |
251 | ```javascript
252 | opentok.setStreamClassLists(app.get('sessionId'), classListArray, function (err) {
253 | if (err) return res.send(500, 'Could not set class lists. Error:' + err.message);
254 | return res.send(200, 'OK');
255 | });
256 | ```
257 |
258 | This sets one stream to have the `focus` class, which causes it to be the large stream
259 | displayed in the composed archive. (This is the behavior of the `horizontalPresentation` and
260 | `verticalPresentation` layout types.) To see this effect, you should open the host and participant
261 | pages on different computers (using different cameras). Or, if you have multiple cameras connected
262 | to your machine, you can use one camera for publishing from the host, and use another for the
263 | participant. Or, if you are using a laptop with an external monitor, you can load the host page
264 | with the laptop closed (no camera) and open the participant page with the laptop open.
265 |
266 | The host client page also uses OpenTok signaling to notify other clients when the layout type and
267 | focus stream changes, and they then update the local display of streams in the HTML DOM accordingly.
268 | However, this is not necessary. The layout of the composed archive is unrelated to the layout of
269 | streams in the web clients.
270 |
271 | When you playback the composed archive, the layout type and focus stream changes, based on calls
272 | to the `OpenTok.setArchiveLayout()` and `OpenTok.setStreamClassLists()` methods during
273 | the recording.
274 |
275 | ### Past Archives
276 |
277 | Start by visiting the history page at . You will see a table that
278 | displays all the archives created with your API Key. If there are more than five, the older ones
279 | can be seen by clicking the "Older →" link. If you click on the name of an archive, your browser
280 | will start downloading the archive file. If you click the "Delete" link in the end of the row
281 | for any archive, that archive will be deleted and no longer available. Some basic information like
282 | when the archive was created, how long it is, and its status is also shown. You should see the
283 | archives you created in the previous sections here.
284 |
285 | We begin to see how this page is created by looking at the route handler for this URL:
286 |
287 | ```javascript
288 | app.get('/history', function(req, res) {
289 | var page = req.param('page') || 1,
290 | offset = (page - 1) * 5;
291 | opentok.listArchives({ offset: offset, count: 5 }, function(err, archives, count) {
292 | if (err) return res.send(500, 'Could not list archives. error=' + err.message);
293 | res.render('history.ejs', {
294 | archives: archives,
295 | showPrevious: page > 1 ? ('/history?page='+(page-1)) : null,
296 | showNext: (count > offset + 5) ? ('/history?page='+(page+1)) : null
297 | });
298 | });
299 | });
300 | ```
301 |
302 | This view is paginated so that we don't potentially show hundreds of rows on the table, which would
303 | be difficult for the user to navigate. So this code starts by figuring out which page needs to be
304 | shown, where each page is a set of 5 archives. The `page` number is read from the request's query
305 | string parameters. The `offset`, which represents how many archives are being skipped, is always
306 | calculated as five times as many pages that are less than the current page, which is
307 | `(page - 1) * 5`. Now there is enough information to ask for a list of archives from OpenTok, which
308 | we do by calling the `listArchives()` method of the `opentok` instance. The first parameter is an
309 | optional object to specify a count and offset. If we are not at the first page, we can pass the view
310 | a string that contains the relative URL for the previous page. Similarly, we can also include one
311 | for the next page. Now the application renders the view using that information and the partial list
312 | of archives.
313 |
314 | At this point the template file `views/history.ejs` handles looping over the array of archives and
315 | outputting the proper information for each column in the table. It also places a link to the
316 | download and delete routes around the archive's name and its delete button, respectively.
317 |
318 | The code for the download route handler is shown below:
319 |
320 | ```javascript
321 | app.get('/download/:archiveId', function(req, res) {
322 | var archiveId = req.param('archiveId');
323 | opentok.getArchive(archiveId, function(err, archive) {
324 | if (err) return res.send(500, 'Could not get archive '+archiveId+'. error='+err.message);
325 | res.redirect(archive.url);
326 | });
327 | });
328 | ```
329 |
330 | The download URL for an archive is available as a property of an `Archive` instance. In order to get
331 | an instance to this archive, the `getArchive()` method of the `opentok` instance is used. The first
332 | parameter is required and it is the `archiveId`. We use the same technique as above to read that
333 | `archiveId` from the URL. The second parameter is a callback function, whose signature has arguments
334 | for the error object and the resulting archive instance. Lastly, we send a redirect response back to
335 | the browser with the archive's URL so the download begins.
336 |
337 | The code for the delete route handler is shown below:
338 |
339 | ```javascript
340 | app.get('/delete/:archiveId', function(req, res) {
341 | var archiveId = req.param('archiveId');
342 | opentok.deleteArchive(archiveId, function(err) {
343 | if (err) return res.send(500, 'Could not stop archive '+archiveId+'. error='+err.message);
344 | res.redirect('/history');
345 | });
346 | });
347 | ```
348 |
349 | Once again the `archiveId` is retrieved from the URL of the request. This value is then passed to the
350 | `deleteArchive()` method of the `opentok` instance. The callback only has an argument for an error
351 | in case one occurred. Now that the archive has been deleted, a redirect response back to the first
352 | page of the history is sent back to the browser.
353 |
354 | That completes the walkthrough for this Archiving sample application. Feel free to continue to use
355 | this application to browse the archives created for your API Key.
356 |
--------------------------------------------------------------------------------
/sample/Archiving/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, no-path-concat */
2 |
3 | // Dependencies
4 | var express = require('express');
5 | var bodyParser = require('body-parser');
6 | var OpenTok = require('../../lib/opentok');
7 | var app = express();
8 |
9 | var opentok;
10 | var apiKey = process.env.API_KEY;
11 | var apiSecret = process.env.API_SECRET;
12 |
13 | // Verify that the API Key and API Secret are defined
14 | if (!apiKey || !apiSecret) {
15 | console.log('You must specify API_KEY and API_SECRET environment variables');
16 | process.exit(1);
17 | }
18 |
19 | // Initialize the express app
20 | app.use(express.static(__dirname + '/public'));
21 | app.use(bodyParser.json()); // for parsing application/json
22 | app.use(bodyParser.urlencoded({
23 | extended: true
24 | }));
25 |
26 | // Starts the express app
27 | function init() {
28 | app.listen(3000, function () {
29 | console.log('You\'re app is now ready at http://localhost:3000/');
30 | });
31 | }
32 |
33 | // Initialize OpenTok
34 | opentok = new OpenTok(apiKey, apiSecret);
35 |
36 | // Create a session and store it in the express app
37 | opentok.createSession({ mediaMode: 'routed' }, function (err, session) {
38 | if (err) throw err;
39 | app.set('sessionId', session.sessionId);
40 | app.set('layout', 'horizontalPresentation');
41 | // We will wait on starting the app until this is done
42 | init();
43 | });
44 |
45 | app.get('/', function (req, res) {
46 | res.render('index.ejs');
47 | });
48 |
49 | app.get('/host', function (req, res) {
50 | var sessionId = app.get('sessionId');
51 | // generate a fresh token for this client
52 | var token = opentok.generateToken(sessionId, {
53 | role: 'moderator',
54 | initialLayoutClassList: ['focus']
55 | });
56 |
57 | res.render('host.ejs', {
58 | apiKey: apiKey,
59 | sessionId: sessionId,
60 | token: token,
61 | focusStreamId: app.get('focusStreamId') || '',
62 | layout: app.get('layout')
63 | });
64 | });
65 |
66 | app.get('/participant', function (req, res) {
67 | var sessionId = app.get('sessionId');
68 | // generate a fresh token for this client
69 | var token = opentok.generateToken(sessionId, { role: 'moderator' });
70 |
71 | res.render('participant.ejs', {
72 | apiKey: apiKey,
73 | sessionId: sessionId,
74 | token: token,
75 | focusStreamId: app.get('focusStreamId') || '',
76 | layout: app.get('layout')
77 | });
78 | });
79 |
80 | app.get('/history', function (req, res) {
81 | var page = req.param('page') || 1;
82 | var offset = (page - 1) * 5;
83 | opentok.listArchives({ offset: offset, count: 5 }, function (err, archives, count) {
84 | if (err) return res.send(500, 'Could not list archives. error=' + err.message);
85 | return res.render('history.ejs', {
86 | archives: archives,
87 | showPrevious: page > 1 ? ('/history?page=' + (page - 1)) : null,
88 | showNext: (count > offset + 5) ? ('/history?page=' + (page + 1)) : null
89 | });
90 | });
91 | });
92 |
93 | app.get('/download/:archiveId', function (req, res) {
94 | var archiveId = req.param('archiveId');
95 | opentok.getArchive(archiveId, function (err, archive) {
96 | if (err) return res.send(500, 'Could not get archive ' + archiveId + '. error=' + err.message);
97 | return res.redirect(archive.url);
98 | });
99 | });
100 |
101 | app.post('/start', function (req, res) {
102 | var hasAudio = (req.param('hasAudio') !== undefined);
103 | var hasVideo = (req.param('hasVideo') !== undefined);
104 | var outputMode = req.param('outputMode');
105 | var archiveOptions = {
106 | name: 'Node Archiving Sample App',
107 | hasAudio: hasAudio,
108 | hasVideo: hasVideo,
109 | outputMode: outputMode
110 | };
111 | if (outputMode === 'composed') {
112 | archiveOptions.layout = { type: 'horizontalPresentation' };
113 | }
114 | opentok.startArchive(app.get('sessionId'), archiveOptions, function (err, archive) {
115 | if (err) {
116 | return res.send(
117 | 500,
118 | 'Could not start archive for session ' + app.get('sessionId') + '. error=' + err.message
119 | );
120 | }
121 | return res.json(archive);
122 | });
123 | });
124 |
125 | app.get('/stop/:archiveId', function (req, res) {
126 | var archiveId = req.param('archiveId');
127 | opentok.stopArchive(archiveId, function (err, archive) {
128 | if (err) return res.send(500, 'Could not stop archive ' + archiveId + '. error=' + err.message);
129 | return res.json(archive);
130 | });
131 | });
132 |
133 | app.get('/delete/:archiveId', function (req, res) {
134 | var archiveId = req.param('archiveId');
135 | opentok.deleteArchive(archiveId, function (err) {
136 | if (err) return res.send(500, 'Could not stop archive ' + archiveId + '. error=' + err.message);
137 | return res.redirect('/history');
138 | });
139 | });
140 |
141 | app.post('/archive/:archiveId/layout', function (req, res) {
142 | var archiveId = req.param('archiveId');
143 | var type = req.body.type;
144 | app.set('layout', type);
145 | opentok.setArchiveLayout(archiveId, type, null, function (err) {
146 | if (err) {
147 | return res.send(500, 'Could not set layout ' + type + '. Error: ' + err.message);
148 | }
149 | return res.send(200, 'OK');
150 | });
151 | });
152 |
153 | app.post('/focus', function (req, res) {
154 | var otherStreams = req.body.otherStreams;
155 | var focusStreamId = req.body.focus;
156 | var classListArray = [];
157 | var i;
158 |
159 | if (otherStreams) {
160 | for (i = 0; i < otherStreams.length; i++) {
161 | classListArray.push({
162 | id: otherStreams[i],
163 | layoutClassList: []
164 | });
165 | }
166 | }
167 | classListArray.push({
168 | id: focusStreamId,
169 | layoutClassList: ['focus']
170 | });
171 | app.set('focusStreamId', focusStreamId);
172 | opentok.setStreamClassLists(app.get('sessionId'), classListArray, function (err) {
173 | if (err) return res.send(500, 'Could not set class lists. Error:' + err.message);
174 | return res.send(200, 'OK');
175 | });
176 | });
177 |
--------------------------------------------------------------------------------
/sample/Archiving/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "opentok-archiving-sample",
3 | "version": "0.0.0",
4 | "description": "Demo of OpenTok API",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "body-parser": "^1.19.0",
13 | "ejs": "^2.5.5",
14 | "express": "^4.16.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/sample/Archiving/public/css/sample.css:
--------------------------------------------------------------------------------
1 | /* Move down content because we have a fixed navbar that is 50px tall */
2 | body {
3 | padding-top: 50px;
4 | padding-bottom: 20px;
5 | background-color: #F2F2F2;
6 | }
7 |
8 | /* Responsive: Portrait tablets and up */
9 | @media screen and (min-width: 768px) {
10 | /* Remove padding from wrapping element since we kick in the grid classes here */
11 | .body-content {
12 | padding: 0;
13 | }
14 | }
15 |
16 | #streams {
17 | background-color: gray;
18 | width: 320px;
19 | height: 240px;
20 | }
21 |
22 | #streams > div {
23 | width: 20%;
24 | height: 20%;
25 | float: left;
26 | position: relative;
27 | cursor: pointer;
28 | }
29 |
30 | #streams.vertical > div {
31 | left: 0px;
32 | clear: left;
33 | padding: 0px;
34 | }
35 |
36 | #streams .focus {
37 | position: relative;
38 | top: 0;
39 | left: 0;
40 | margin-top: 0;
41 | height: 80%;
42 | width: 100%;
43 | }
44 |
45 | #streams.vertical .focus {
46 | padding: 0;
47 | left: 0;
48 | margin: 0;
49 | left: 20%;
50 | height: 100%;
51 | width: 80%;
52 | }
53 |
54 | .stop {
55 | display: none;
56 | }
57 |
58 | .bump-me {
59 | padding-top: 40px;
60 | }
61 |
--------------------------------------------------------------------------------
/sample/Archiving/public/img/archiving-off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opentok/opentok-node/778f82737dd56102e5c95b9a51c7c6d05b48e41d/sample/Archiving/public/img/archiving-off.png
--------------------------------------------------------------------------------
/sample/Archiving/public/img/archiving-on-idle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opentok/opentok-node/778f82737dd56102e5c95b9a51c7c6d05b48e41d/sample/Archiving/public/img/archiving-on-idle.png
--------------------------------------------------------------------------------
/sample/Archiving/public/img/archiving-on-message.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opentok/opentok-node/778f82737dd56102e5c95b9a51c7c6d05b48e41d/sample/Archiving/public/img/archiving-on-message.png
--------------------------------------------------------------------------------
/sample/Archiving/public/js/host.js:
--------------------------------------------------------------------------------
1 | /* global OT, apiKey, sessionId, token, $, layout, focusStreamId */
2 | /* eslint-disable no-console */
3 |
4 | var session = OT.initSession(apiKey, sessionId);
5 | var publisher = OT.initPublisher('publisher', {
6 | insertMode: 'append',
7 | width: '100%',
8 | height: '100%'
9 | });
10 | var archiveID = null;
11 |
12 | function disableForm() {
13 | $('.archive-options-fields').attr('disabled', 'disabled');
14 | }
15 |
16 | function enableForm() {
17 | $('.archive-options-fields').removeAttr('disabled');
18 | }
19 |
20 | function positionStreams() {
21 | var $focusElement;
22 | $focusElement = $('.focus');
23 | if ($('#streams').hasClass('vertical')) {
24 | $('#streams').children().css('top', '0');
25 | $focusElement.appendTo('#streams');
26 | $focusElement.css('top', (-20 * ($('#streams').children().size() - 1)) + '%');
27 | }
28 | else {
29 | $focusElement.prependTo('#streams');
30 | $focusElement.css('top', '0');
31 | }
32 | }
33 |
34 | function setFocus(focusStreamId) {
35 | var $focusElement;
36 | var otherStreams = $.map($('#streams').children(), function (element) {
37 | var streamId = (element.id === 'publisher' && publisher.stream) ? publisher.stream.streamId
38 | : element.id;
39 | if (streamId !== focusStreamId) {
40 | $('#' + element.id).removeClass('focus');
41 | return streamId;
42 | }
43 | return null;
44 | });
45 |
46 | $.post('/focus', {
47 | focus: focusStreamId,
48 | otherStreams: otherStreams
49 | }).done(function () {
50 | console.log('Focus changed.');
51 | }).fail(function (jqXHR, textStatus, errorThrown) {
52 | console.error('Stream class list error:', errorThrown);
53 | });
54 |
55 | $('.focus').removeClass('focus');
56 | $focusElement = (publisher.stream && publisher.stream.streamId === focusStreamId) ?
57 | $('#publisher') : $('#' + focusStreamId);
58 | $focusElement.addClass('focus');
59 | session.signal({
60 | type: 'focusStream',
61 | data: focusStreamId
62 | });
63 | positionStreams();
64 | }
65 |
66 | function createFocusClick(elementId, focusStreamId) {
67 | $('#' + elementId).click(function () {
68 | setFocus(focusStreamId);
69 | });
70 | }
71 |
72 | if (layout === 'verticalPresentation') {
73 | $('#streams').addClass('vertical');
74 | }
75 |
76 | session.connect(token, function (err) {
77 | if (err) {
78 | alert(err.message || err); // eslint-disable-line no-alert
79 | }
80 | session.publish(publisher);
81 | });
82 |
83 | publisher.on('streamCreated', function () {
84 | createFocusClick(publisher.id, publisher.stream.streamId);
85 | positionStreams();
86 | });
87 |
88 | session.on('streamCreated', function (event) {
89 | var subscriber;
90 | var streamId = event.stream.streamId;
91 | var $streamContainer = $('');
92 | $streamContainer.attr('id', event.stream.id);
93 | $('#streams').append($streamContainer);
94 | subscriber = session.subscribe(event.stream, streamId, {
95 | insertMode: 'append',
96 | width: '100%',
97 | height: '100%'
98 | });
99 |
100 | if (streamId === focusStreamId) {
101 | setFocus(streamId);
102 | }
103 | createFocusClick(subscriber.id, streamId);
104 | positionStreams();
105 | });
106 |
107 | session.on('streamDestroyed', function (event) {
108 | var $streamElem = $('#' + event.stream.id);
109 | if ($streamElem.hasClass('focus')) {
110 | setFocus(publisher.stream.streamId);
111 | }
112 | $streamElem.remove();
113 | positionStreams();
114 | });
115 |
116 | session.on('archiveStarted', function (event) {
117 | archiveID = event.id;
118 | console.log('ARCHIVE STARTED');
119 | $('.start').hide();
120 | $('.stop').show();
121 | disableForm();
122 | });
123 |
124 | session.on('archiveStopped', function () {
125 | archiveID = null;
126 | console.log('ARCHIVE STOPPED');
127 | $('.start').show();
128 | $('.stop').hide();
129 | enableForm();
130 | });
131 |
132 | $(document).ready(function () {
133 | $('.start').click(function () {
134 | var options = $('.archive-options').serialize();
135 | disableForm();
136 | $.post('/start', options)
137 | .fail(enableForm);
138 | }).prop('disabled', false);
139 | $('.stop').click(function () {
140 | $.get('stop/' + archiveID);
141 | });
142 | $('.toggle-layout').click(function () {
143 | var newLayoutClass;
144 |
145 | if ($('#streams').hasClass('vertical')) {
146 | $('#streams').removeClass('vertical');
147 | }
148 | else {
149 | $('#streams').addClass('vertical');
150 | }
151 |
152 | positionStreams();
153 |
154 | newLayoutClass = $('#streams').hasClass('vertical') ? 'verticalPresentation'
155 | : 'horizontalPresentation';
156 |
157 | if (archiveID) {
158 | $.post('archive/' + archiveID + '/layout', {
159 | type: newLayoutClass
160 | }).done(function () {
161 | console.log('Archive layout updated.');
162 | }).fail(function (jqXHR) {
163 | console.error('Archive layout error:', jqXHR.responseText);
164 | });
165 | }
166 |
167 | session.signal({
168 | type: 'layoutClass',
169 | data: newLayoutClass
170 | });
171 | });
172 | });
173 |
--------------------------------------------------------------------------------
/sample/Archiving/public/js/participant.js:
--------------------------------------------------------------------------------
1 | /* global OT, apiKey, sessionId, token, $, layout, focusStreamId */
2 | var session = OT.initSession(apiKey, sessionId);
3 | var publisher;
4 |
5 | var container = $('');
6 |
7 | if (layout === 'verticalPresentation') {
8 | $('#streams').addClass('vertical');
9 | }
10 |
11 | container.addClass('focus');
12 | $('#streams').append(container);
13 |
14 | publisher = OT.initPublisher('publisher', {
15 | insertMode: 'append',
16 | width: '100%',
17 | height: '100%'
18 | });
19 |
20 | function positionStreams() {
21 | var $focusElement = $('.focus');
22 | if ($('#streams').hasClass('vertical')) {
23 | $focusElement.appendTo('#streams');
24 | $('#streams').children().css('top', '0');
25 | $focusElement.css('top', (-20 * ($('#streams').children().size() - 1)) + '%');
26 | }
27 | else {
28 | $focusElement.prependTo('#streams');
29 | $focusElement.css('top', '0');
30 | }
31 | }
32 |
33 | function focusStream(streamId) {
34 | var focusStreamId = streamId;
35 | var $focusElement = (publisher.stream && publisher.stream.id === focusStreamId) ? $('#publisher')
36 | : $('#' + focusStreamId);
37 | $('.focus').removeClass('focus');
38 | $focusElement.addClass('focus');
39 | positionStreams();
40 | }
41 |
42 | session.connect(token, function (err) {
43 | if (err) {
44 | alert(err.message || err); // eslint-disable-line no-alert
45 | }
46 | session.publish(publisher);
47 | });
48 |
49 | session.on('streamCreated', function (event) {
50 | var streamId = event.stream.id;
51 | container = document.createElement('div');
52 | container.id = streamId;
53 | $('#streams').append(container);
54 | session.subscribe(event.stream, streamId, {
55 | insertMode: 'append',
56 | width: '100%',
57 | height: '100%'
58 | });
59 | if (streamId === focusStreamId) {
60 | focusStream(streamId);
61 | }
62 | positionStreams();
63 | });
64 |
65 | session.on('streamDestroyed', function (event) {
66 | $('#' + event.stream.id).remove();
67 | positionStreams();
68 | });
69 |
70 | session.on('signal:layoutClass', function (event) {
71 | if (event.data === 'horizontalPresentation') {
72 | $('#streams').removeClass('vertical');
73 | $('.focus').prependTo('#streams');
74 | }
75 | else {
76 | $('#streams').addClass('vertical');
77 | $('.focus').appendTo('#streams');
78 | }
79 | positionStreams();
80 | });
81 |
82 | session.on('signal:focusStream', function (event) {
83 | focusStream(event.data);
84 | });
85 |
--------------------------------------------------------------------------------
/sample/Archiving/views/footer.ejs:
--------------------------------------------------------------------------------
1 |
2 |