├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPING.md ├── LICENSE ├── README.md ├── box.json ├── build.xml ├── composer.json ├── phpcs.xml ├── phpdoc.dist.xml ├── phpstan.neon ├── phpunit.xml.dist ├── rector.yaml ├── sample ├── Archiving │ ├── .gitignore │ ├── README.md │ ├── composer.json │ ├── run-demo │ ├── run-demo.bat │ ├── templates │ │ ├── base.html │ │ ├── history.html │ │ ├── host.html │ │ ├── index.html │ │ └── participant.html │ └── web │ │ ├── css │ │ └── sample.css │ │ ├── img │ │ ├── archiving-off.png │ │ ├── archiving-on-idle.png │ │ └── archiving-on-message.png │ │ ├── index.php │ │ └── js │ │ ├── host.js │ │ └── participant.js ├── HelloWorld │ ├── .gitignore │ ├── README.md │ ├── composer.json │ ├── run-demo │ ├── run-demo.bat │ ├── templates │ │ └── helloworld.php │ └── web │ │ ├── index.php │ │ └── js │ │ └── helloworld.js └── SipCall │ ├── .gitignore │ ├── README.md │ ├── composer.json │ ├── run-demo │ ├── run-demo.bat │ ├── templates │ └── index.php │ └── web │ ├── index.php │ ├── js │ └── index.js │ └── stylesheets │ ├── pattern.css │ └── style.css ├── src └── OpenTok │ ├── Archive.php │ ├── ArchiveList.php │ ├── ArchiveMode.php │ ├── Broadcast.php │ ├── Exception │ ├── ArchiveAuthenticationException.php │ ├── ArchiveDomainException.php │ ├── ArchiveException.php │ ├── ArchiveUnexpectedValueException.php │ ├── AuthenticationException.php │ ├── BroadcastAuthenticationException.php │ ├── BroadcastDomainException.php │ ├── BroadcastException.php │ ├── BroadcastUnexpectedValueException.php │ ├── DomainException.php │ ├── Exception.php │ ├── ForceDisconnectAuthenticationException.php │ ├── ForceDisconnectConnectionException.php │ ├── ForceDisconnectException.php │ ├── ForceDisconnectUnexpectedValueException.php │ ├── InvalidArgumentException.php │ ├── SignalAuthenticationException.php │ ├── SignalConnectionException.php │ ├── SignalException.php │ ├── SignalNetworkConnectionException.php │ ├── SignalUnexpectedValueException.php │ └── UnexpectedValueException.php │ ├── Layout.php │ ├── MediaMode.php │ ├── OpenTok.php │ ├── OutputMode.php │ ├── Render.php │ ├── Role.php │ ├── Session.php │ ├── SipCall.php │ ├── Stream.php │ ├── StreamList.php │ ├── StreamMode.php │ └── Util │ ├── BasicEnum.php │ ├── Client.php │ ├── Validators.php │ ├── archive-schema.json │ └── broadcast-schema.json ├── tests ├── OpenTokTest │ ├── ArchiveTest.php │ ├── BroadcastTest.php │ ├── LayoutTest.php │ ├── OpenTokTest.php │ ├── RenderTest.php │ ├── SessionTest.php │ ├── SipCallTest.php │ ├── TestHelpers.php │ ├── Util │ │ ├── ClientTest.php │ │ └── responses │ │ │ ├── connect.json │ │ │ ├── signal-failure-invalid-token.json │ │ │ ├── signal-failure-no-clients.json │ │ │ ├── signal-failure-payload.json │ │ │ └── signal-failure.json │ ├── Validators │ │ └── ValidatorsTest.php │ └── test.key ├── bootstrap.php └── mock │ ├── session │ └── create │ │ ├── alwaysarchived │ │ ├── relayed │ │ └── routed_location-12.34.56.78 │ └── v2 │ └── project │ └── APIKEY │ ├── archive │ ├── ARCHIVEID │ │ ├── get │ │ ├── get-expired │ │ └── stop │ ├── get │ ├── get_second │ ├── get_third │ ├── manual_mode_session │ ├── session │ ├── session_hasVideo-false │ ├── session_name-showtime │ ├── session_outputMode-individual │ ├── session_resolution-hd │ └── session_resolution-sd │ ├── broadcast │ ├── BROADCASTID │ │ ├── get │ │ ├── layout │ │ │ ├── get │ │ │ ├── type-custom │ │ │ └── type-pip │ │ ├── start_default │ │ ├── start_dvr │ │ ├── start_ll │ │ └── stop │ ├── session_layout-bestfit │ └── session_manual_stream │ ├── connect │ ├── dial │ ├── dialForceMute │ ├── render │ ├── render_get │ ├── render_list │ └── render_start │ └── session │ └── SESSIONID │ ├── caption-start │ ├── caption-stop │ ├── mute │ ├── play-dtmf │ ├── play-dtmf-400 │ └── stream │ ├── STREAMID │ ├── get │ └── layoutClassList │ └── get └── tools └── stub.php /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | 7 | ## Current Behavior 8 | 9 | 10 | 11 | ## Possible Solution 12 | 13 | 14 | 15 | ## Steps to Reproduce (for bugs) 16 | 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 4. 22 | 23 | ## Context 24 | 25 | 26 | 27 | ## Your Environment 28 | 29 | * Version used: 30 | * Environment name and version (e.g. PHP 5.4 on nginx 1.9.1): 31 | * Operating System and version: 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | 14 | 15 | ## Example Output or Screenshots (if appropriate): 16 | 17 | ## Types of changes 18 | 19 | - [ ] Bug fix (non-breaking change which fixes an issue) 20 | - [ ] New feature (non-breaking change which adds functionality) 21 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 22 | 23 | ## Checklist: 24 | 25 | 26 | - [ ] My code follows the code style of this project. 27 | - [ ] My change requires a change to the documentation. 28 | - [ ] I have updated the documentation accordingly. 29 | - [ ] I have read the **CONTRIBUTING** document. 30 | - [ ] I have added tests to cover my changes. 31 | - [ ] All new and existing tests passed. 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: 6 | - ubuntu-latest 7 | strategy: 8 | matrix: 9 | php: ['8.1', '8.2', '8.3', '8.4'] 10 | steps: 11 | - name: Configure Git 12 | if: ${{ matrix.os == 'windows-latest' }} 13 | run: | 14 | git config --system core.autocrlf false 15 | git config --ystem core.eol lf 16 | 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php }} 24 | extensions: json 25 | tools: composer 26 | coverage: xdebug 27 | 28 | - name: Get Composer cache directory 29 | id: composercache 30 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 31 | 32 | - name: Cache Composer dependencies 33 | uses: actions/cache@v4 34 | with: 35 | path: ${{ steps.composercache.outputs.dir }} 36 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 37 | restore-keys: ${{ runner.os }}-composer- 38 | 39 | - name: Install dependencies 40 | run: composer install --no-interaction --prefer-dist --no-progress --no-suggest ${{ matrix.composer-options }} 41 | 42 | - name: Analyze & test 43 | run: | 44 | vendor/bin/phpunit -v --configuration ./phpunit.xml.dist --coverage-clover=coverage.xml 45 | 46 | - name: Run codecov 47 | uses: codecov/codecov-action@v1 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | composer.phar 3 | composer.lock 4 | opentok.phar 5 | vendor/ 6 | phpunit.xml 7 | sample/*/vendor/ 8 | sample/*/cache/ 9 | docs/ 10 | .phpdoc/ 11 | phpDocumentor* 12 | .phpunit.result.cache 13 | .idea/* -------------------------------------------------------------------------------- /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 | ## Tools 8 | 9 | * [Phing](http://www.phing.info/): used to run predefined tasks. Installed via Composer into the vendor directory. You 10 | can run phing but using the command line script `./vendor/bin/phing` or you can put it on your PATH. 11 | * [Composer](https://getcomposer.org/): used to manage dependencies for the project. 12 | * [Box](http://box-project.org/): used to generate a phar archive, which is useful for users who 13 | don't use Composer in their own project. 14 | 15 | ## Tasks 16 | 17 | ### Testing 18 | 19 | This project's tests are written as PHPUnit test cases. Common tasks: 20 | 21 | * `./vendor/bin/phing test` - run the test suite. 22 | 23 | ### Building docs 24 | 25 | Install [PhpDocumentor](https://docs.phpdoc.org/). Add it to the project root directory. 26 | Then run `php phpDocumentor.phar --config phpdoc.dist.xml`. 27 | 28 | The current reference docs were built with PhpDocumentor 3.3.0. 29 | 30 | ### Releasing 31 | 32 | In order to create a release, the following should be completed in order. 33 | 34 | 1. Ensure all the tests are passing (`./vendor/bin/phing test`) and that there is enough test coverage. 35 | 1. Make sure you are on the `master` branch of the repository, with all changes merged/commited already. 36 | 1. Update the version number in the source code and the README. See [Versioning](#versioning) for information 37 | about selecting an appropriate version number. Files to inspect for possible need to change: 38 | - src/OpenTok/Util/Client.php 39 | - tests/OpenTok/OpenTokTest.php 40 | - tests/OpenTok/ArchiveTest.php 41 | - README.md (only needs to change when MINOR version is changing) 42 | 1. Commit the version number change with the message "Update to version x.x.x", substituting the new version number. 43 | 1. Create a git tag: `git tag -a vx.x.x -m "Release vx.x.x"` 44 | 1. Change the version number for future development by incrementing the PATH number and adding 45 | "-alpha.1" in each file except samples and documentation. Then make another commit with the 46 | message "Begin development on next version". 47 | 1. Push the changes to the source repository: `git push origin master; git push --tags origin`s 48 | 1. Generate a phar archive for distribution using [Box](https://github.com/box-project/box2): `box build`. Be sure that the 49 | dependencies in the `/vendor` directory are current before building. Upload it to the GitHub Release. Add 50 | release notes with a description of changes and fixes. 51 | 52 | ## Workflow 53 | 54 | ### Versioning 55 | 56 | The project uses [semantic versioning](http://semver.org/) as a policy for incrementing version numbers. For planned 57 | work that will go into a future version, there should be a Milestone created in the Github Issues named with the version 58 | number (e.g. "v2.2.1"). 59 | 60 | During development the version number should end in "-alpha.x" or "-beta.x", where x is an increasing number starting from 1. 61 | 62 | ### Branches 63 | 64 | * `master` - the main development branch. 65 | * `feat.foo` - feature branches. these are used for longer running tasks that cannot be accomplished in one commit. 66 | once merged into master, these branches should be deleted. 67 | * `vx.x.x` - if development for a future version/milestone has begun while master is working towards a sooner 68 | release, this is the naming scheme for that branch. once merged into master, these branches should be deleted. 69 | 70 | ### Tags 71 | 72 | * `vx.x.x` - commits are tagged with a final version number during release. 73 | 74 | ### Issues 75 | 76 | Issues are labelled to help track their progress within the pipeline. 77 | 78 | * no label - these issues have not been triaged. 79 | * `bug` - confirmed bug. aim to have a test case that reproduces the defect. 80 | * `enhancement` - contains details/discussion of a new feature. it may not yet be approved or placed into a 81 | release/milestone. 82 | * `wontfix` - closed issues that were never addressed. 83 | * `duplicate` - closed issue that is the same to another referenced issue. 84 | * `question` - purely for discussion 85 | 86 | ### Management 87 | 88 | When in doubt, find the maintainers and ask. 89 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "alias": "opentok", 3 | "directories": [ "src" ], 4 | "files": [ "tools/stub.php" ], 5 | "finder": [ 6 | { 7 | "name": ["*.php", "*.pem"], 8 | "exclude": ["Tests", "tests"], 9 | "in": "vendor" 10 | } 11 | ], 12 | "output": "opentok.phar", 13 | "stub": "tools/stub.php" 14 | } 15 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opentok/opentok", 3 | "description": "OpenTok is a platform for creating real time streaming video applications, created by TokBox.", 4 | "type": "library", 5 | "keywords": [ 6 | "TokBox", 7 | "OpenTok", 8 | "PHP", 9 | "WebRTC", 10 | "video", 11 | "streaming" 12 | ], 13 | "homepage": "https://github.com/opentok/Opentok-PHP-SDK", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Ankur Oberoi", 18 | "email": "ankur@tokbox.com", 19 | "role": "Developer" 20 | }, 21 | { 22 | "name": "Community contributors", 23 | "homepage": "https://github.com/opentok/Opentok-PHP-SDK/graphs/contributors" 24 | } 25 | ], 26 | "support": { 27 | "email": "support@tokbox.com", 28 | "issues": "https://github.com/opentok/Opentok-PHP-SDK/issues" 29 | }, 30 | "require": { 31 | "php": "^7.2|^8.0", 32 | "ext-xml": "*", 33 | "johnstevenson/json-works": "~1.1", 34 | "firebase/php-jwt": "^6.11", 35 | "guzzlehttp/guzzle": "~6.0|~7.0", 36 | "ext-json": "*", 37 | "vonage/jwt": "^0.5.1" 38 | }, 39 | "require-dev": { 40 | "phpunit/phpunit": "^7.4|^8.0", 41 | "php-http/mock-client": "^1.4", 42 | "helmich/phpunit-json-assert": "^3.0.0", 43 | "squizlabs/php_codesniffer": "^3.1", 44 | "php-http/guzzle7-adapter": "^1.0", 45 | "phpstan/phpstan": "^0.12", 46 | "rector/rector": "^0.8", 47 | "phing/phing": "~2.16.0" 48 | }, 49 | "scripts": { 50 | "phpstan": "./vendor/bin/phpstan", 51 | "phpcs": "./vendor/bin/phpcs", 52 | "test": "./vendor/bin/phpunit --testdox" 53 | }, 54 | "autoload": { 55 | "psr-4": { 56 | "OpenTok\\": "src/OpenTok", 57 | "OpenTokTest\\": "tests/OpenTokTest" 58 | } 59 | }, 60 | "config": { 61 | "allow-plugins": { 62 | "php-http/discovery": true 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./src 5 | 6 | -------------------------------------------------------------------------------- /phpdoc.dist.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | docs 10 | 11 | 12 | 13 | 14 | src 15 | 16 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - src -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tests/ 7 | 8 | 9 | 10 | 11 | 12 | src/ 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /rector.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | php_version_features: '7.1' 3 | import_short_classes: false 4 | sets: 5 | - dead-code -------------------------------------------------------------------------------- /sample/Archiving/.gitignore: -------------------------------------------------------------------------------- 1 | .htaccess 2 | web/cache/ -------------------------------------------------------------------------------- /sample/Archiving/README.md: -------------------------------------------------------------------------------- 1 | # OpenTok Archiving Sample for PHP 2 | 3 | This is a simple demo app that shows how you can use the OpenTok Java 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 [Composer](http://getcomposer.org) in this directory as well 9 | as the root SDK directory 10 | 11 | ``` 12 | $ cd ../../ 13 | $ composer.phar install 14 | $ cd sample/Archiving 15 | $ ../../composer.phar install 16 | ``` 17 | 18 | Next, input your own API Key and API Secret into the `run-demo` script file: 19 | 20 | ``` 21 | export API_KEY=0000000 22 | export API_SECRET=b60d0b2568f3ea9731bd9d3f71be263ce19f802f 23 | ``` 24 | 25 | Finally, start the PHP CLI development server (requires PHP >= 5.4) using the `run-demo` script 26 | 27 | ``` 28 | $ ./run-demo 29 | ``` 30 | 31 | Visit in your browser. You can now create new archives (either as a host or 32 | as a participant) and also play archives that have already been created. 33 | 34 | ## Walkthrough 35 | 36 | This demo application uses the same frameworks and libraries as the HelloWorld sample. If you have 37 | not already gotten familiar with the code in that project, consider doing so before continuing. 38 | 39 | The explanations below are separated by page. Each section will focus on a route handler within the 40 | main application (web/index.php). 41 | 42 | ### Creating Archives – Host View 43 | 44 | Start by visiting the host page at and using the application to record 45 | an archive. Your browser will first ask you to approve permission to use the camera and microphone. 46 | Once you've accepted, your image will appear inside the section titled 'Host'. To start recording 47 | the video stream, press the 'Start Archiving' button. Once archiving has begun the button will turn 48 | green and change to 'Stop Archiving'. You should also see a red blinking indicator that you are 49 | being recorded. Wave and say hello! Stop archiving when you are done. 50 | 51 | Next we will see how the host view is implemented on the server. The route handler for this page is 52 | shown below: 53 | 54 | ```php 55 | $app->get('/host', function () use ($app, $sessionId) { 56 | 57 | $token = $app->opentok->generateToken($sessionId, array( 58 | 'role' => Role::MODERATOR 59 | ), true); 60 | 61 | $app->render('host.html', array( 62 | 'apiKey' => $app->apiKey, 63 | 'sessionId' => $sessionId, 64 | 'token' => $token 65 | )); 66 | }); 67 | ``` 68 | 69 | If you've completed the HelloWorld walkthrough, this should look familiar. This handler simply 70 | generates the three strings that the client (JavaScript) needs to connect to the session: `apiKey`, 71 | `sessionId` and `token`. After the user has connected to the session, they press the 72 | 'Start Archiving' button, which sends an XHR (or Ajax) request to the 73 | URL. The route handler for this URL is shown below: 74 | 75 | ```php 76 | $app->post('/start', function () use ($app, $sessionId) { 77 | 78 | $archive = $app->opentok->startArchive($sessionId, array( 79 | 'name' => "PHP Archiving Sample App", 80 | 'hasAudio' => ($app->request->post('hasAudio') == 'on'), 81 | 'hasVideo' => ($app->request->post('hasVideo') == 'on'), 82 | 'outputMode' => ($app->request->post('outputMode') == 'composed' ? OutputMode::COMPOSED : OutputMode::INDIVIDUAL) 83 | )); 84 | 85 | $app->response->headers->set('Content-Type', 'application/json'); 86 | echo $archive->toJson(); 87 | }); 88 | ``` 89 | 90 | In this handler, the `startArchive()` method of the `opentok` instance is called with the 91 | `sessionId` for the session that needs to be archived. The remaining arguments are a set of 92 | optional properties for the archive. The `name` is stored with the archive and can be read later. 93 | The `hasAudio`, `hasVideo`, and `outputMode` values are read from the request body; these define 94 | whether the archive will record audio and video, and whether it will record streams individually or 95 | to a single file composed of all streams. In this case, as in the HelloWorld sample app, there is 96 | only one session created and it is used here and for the participant view. This will trigger the 97 | recording to begin. The response sent back to the client's XHR request will be the JSON 98 | representation of the archive, which is serialized by the `toJson()` method. The client is also 99 | listening for the `archiveStarted` event, and uses that event to change the 'Start Archiving' button 100 | to show 'Stop Archiving' instead. When the user presses the button this time, another XHR request 101 | is sent to the URL where `:archiveId` represents the ID the 102 | client receives in the 'archiveStarted' event. The route handler for this request is shown below: 103 | 104 | ```php 105 | $app->get('/stop/:archiveId', function ($archiveId) use ($app) { 106 | $archive = $app->opentok->stopArchive($archiveId); 107 | $app->response->headers->set('Content-Type', 'application/json'); 108 | echo $archive->toJson(); 109 | }); 110 | ``` 111 | 112 | This handler is very similar to the previous one. Instead of calling the `startArchive()` method, 113 | the `stopArchive()` method is called. This method takes an `archiveId` as its parameter, which 114 | is different for each time a session starts recording. But the client has sent this to the server 115 | as part of the URL, so the `$archiveId` argument from the route is used to retrieve it. 116 | 117 | Now you have understood the three main routes that are used to create the Host experience of 118 | creating an archive. Much of the functionality is done in the client with JavaScript. That code can 119 | be found in the `web/js/host.js` file. Read about the 120 | [OpenTok.js JavaScript](http://tokbox.com/opentok/libraries/client/js/) library to learn more. 121 | 122 | ### Creating Archives - Participant View 123 | 124 | With the host view still open and publishing, open an additional window or tab and navigate to 125 | and allow the browser to use your camera and microphone. Once 126 | again, start archiving in the host view. Back in the participant view, notice that the red blinking 127 | indicator has been shown so that the participant knows his video is being recorded. Now stop the 128 | archiving in the host view. Notice that the indicator has gone away in the participant view too. 129 | 130 | Creating this view on the server is as simple as the HelloWorld sample application. See the code 131 | for the route handler below: 132 | 133 | ```php 134 | $app->get('/participant', function () use ($app, $sessionId) { 135 | 136 | $token = $app->opentok->generateToken($sessionId, array( 137 | 'role' => Role::MODERATOR 138 | )); 139 | 140 | $app->render('participant.html', array( 141 | 'apiKey' => $app->apiKey, 142 | 'sessionId' => $sessionId, 143 | 'token' => $token 144 | )); 145 | }); 146 | ``` 147 | 148 | Since this view has no further interactivity with buttons, this is all that is needed for a client 149 | that is participating in an archived session. Once again, much of the functionality is implemented 150 | in the client, in code that can be found in the `web/js/participant.js` file. 151 | 152 | ### Past Archives 153 | 154 | Start by visiting the history page at . You will see a table that 155 | displays all the archives created with your API Key. If there are more than five, the older ones 156 | can be seen by clicking the "Older →" link. If you click on the name of an archive, your browser 157 | will start downloading the archive file. If you click the "Delete" link in the end of the row 158 | for any archive, that archive will be deleted and no longer available. Some basic information like 159 | when the archive was created, how long it is, and its status is also shown. You should see the 160 | archives you created in the previous sections here. 161 | 162 | We begin to see how this page is created by looking at the route handler for this URL: 163 | 164 | ```php 165 | $app->get('/history', function () use ($app) { 166 | $page = intval($app->request->get('page')); 167 | if (empty($page)) { 168 | $page = 1; 169 | } 170 | 171 | $offset = ($page - 1) * 5; 172 | 173 | $archives = $app->opentok->listArchives($offset, 5); 174 | 175 | $toArray = function ($archive) { 176 | return $archive->toArray(); 177 | }; 178 | 179 | $app->render('history.html', array( 180 | 'archives' => array_map($toArray, $archives->getItems()), 181 | 'showPrevious' => $page > 1 ? '/history?page='.($page-1) : null, 182 | 'showNext' => $archives->totalCount() > $offset + 5 ? '/history?page='.($page+1) : null 183 | )); 184 | }); 185 | ``` 186 | 187 | This view is paginated so that we don't potentially show hundreds of rows on the table, which would 188 | be difficult for the user to navigate. So this code starts by figuring out which page needs to be 189 | shown, where each page is a set of 5 archives. The `page` number is read from the request's query 190 | string parameters as a string and then coerced into an `int`. If the value was missing, its assigned 191 | to the default value of one. The `offset`, which represents how many archives are being skipped, is 192 | always calculated as five times as many pages that are less than the current page, which is 193 | `($page - 1) * 5`. Now there is enough information to ask for a list of archives from OpenTok, 194 | which we do by calling the `listArchives()` method of the `opentok` instance. The first parameter is 195 | the offset, and the second is the count (which is always 5 in this view). If we are not 196 | at the first page, we can pass the view a string that contains the relative URL for the previous 197 | page. Similarly, we can also include one for the next page. Now the application renders the view 198 | using that information and the partial list of archives. 199 | 200 | At this point the template file `templates/history.html` handles looping over the array of archives 201 | and outputting the proper information for each column in the table. It also places a link to the 202 | download and delete routes around the archive's name and its delete button, respectively. 203 | 204 | The code for the download route handler is shown below: 205 | 206 | ```php 207 | $app->get('/download/:archiveId', function ($archiveId) use ($app) { 208 | $archive = $app->opentok->getArchive($archiveId); 209 | $app->redirect($archive->url); 210 | }); 211 | ``` 212 | 213 | The download URL for an archive is available as a property of an `Archive` instance. In order to get 214 | an instance to this archive, the `getArchive()` method of the `opentok` instance is used. The only 215 | parameter it needs is the `archiveId`. We use the same technique as above to read that `archiveId` 216 | from the URL. Lastly, we send a redirect response back to the browser so the download begins. 217 | 218 | The code for the delete route handler is shown below: 219 | 220 | ```php 221 | $app->get('/delete/:archiveId', function ($archiveId) use ($app) { 222 | $app->opentok->deleteArchive($archiveId); 223 | $app->redirect('/history'); 224 | }); 225 | ``` 226 | 227 | Once again the `archiveId` is retrieved from the URL of the request. This value is then passed to the 228 | `deleteArchive()` method of the `opentok` instance. Now that the archive has been deleted, a 229 | redirect response back to the first page of the history is sent back to the browser. 230 | 231 | That completes the walkthrough for this Archiving sample application. Feel free to continue to use 232 | this application to browse the archives created for your API Key. 233 | -------------------------------------------------------------------------------- /sample/Archiving/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "slim/slim": "2.*", 4 | "slim/views": "0.1.*", 5 | "twig/twig": "1.*", 6 | "gregwar/cache": "1.0.*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /sample/Archiving/run-demo: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$API_KEY" ] || [ -z "$API_SECRET" ] 4 | then 5 | export API_KEY= 6 | export API_SECRET= 7 | fi 8 | 9 | if [ -d "cache" ] 10 | then 11 | rm -rf cache/ 12 | fi 13 | 14 | php -S localhost:8080 -t web/ 15 | -------------------------------------------------------------------------------- /sample/Archiving/run-demo.bat: -------------------------------------------------------------------------------- 1 | :: Why? because windows can't do an OR within the conditional 2 | IF NOT DEFINED API_KEY GOTO defkeysecret 3 | IF NOT DEFINED API_SECRET GOTO defkeysecret 4 | GOTO skipdef 5 | 6 | :defkeysecret 7 | 8 | SET API_KEY= 9 | SET API_SECRET= 10 | 11 | :skipdef 12 | 13 | RD /q /s cache 14 | 15 | php.exe -S localhost:8080 -t web web/index.php 16 | -------------------------------------------------------------------------------- /sample/Archiving/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Archiving Sample 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | {% block content %}{% endblock %} 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sample/Archiving/templates/history.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 |
5 | 6 |
7 | 8 |
9 |
10 |

Past Recordings

11 |
12 |
13 | {% if archives|length > 0 %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for item in archives %} 25 | 26 | 27 | 36 | 37 | 38 | 39 | 46 | 47 | 48 | {% endfor %} 49 | 50 |
 CreatedDurationStatus
28 | {% if item.status == "available" and item.url is defined and item.url|length > 0 %} 29 | 30 | {% endif %} 31 | {{ item.name|default("Untitled") }} 32 | {% if item.status == "available" and item.url is defined and item.url|length > 0 %} 33 | 34 | {% endif %} 35 | {{ (item.createdAt//1000)|date }}{{ item.duration }} seconds{{ item.status }} 40 | {% if item.status == "available" %} 41 | Delete 42 | {% else %} 43 |   44 | {% endif %} 45 |
51 | {% else %} 52 |

53 | There are no archives currently. Try making one in the host view. 54 |

55 | {% endif %} 56 |
57 | 66 |
67 |
68 |
69 | 70 | {% endblock %} 71 | -------------------------------------------------------------------------------- /sample/Archiving/templates/host.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | 5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 |

Host

13 |
14 |
15 |
16 |
17 | 44 |
45 |
46 | 47 |
48 |
49 |

Instructions

50 |
51 |
52 |

53 | Click Start archiving to begin archiving this session. 54 | All publishers in the session will be included, and all publishers that 55 | join the session will be included as well. 56 |

57 |

58 | Click Stop archiving to end archiving this session. 59 | You can then go to past archives to 60 | view your archive (once its status changes to available). 61 |

62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
WhenYou will see
Archiving is started
Archiving remains on
Archiving is stopped
84 |
85 |
86 |
87 | 88 | 93 | 94 | 95 | 96 | 97 | {% endblock %} 98 | -------------------------------------------------------------------------------- /sample/Archiving/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 |
5 | 6 | 7 |
8 | 9 | 10 |
11 |
12 | 13 |
14 |
Create an archive
15 |
16 |

17 | Everyone who joins either the Host View or Participant View 18 | joins a single OpenTok session. Anyone with the Host View 19 | open can click Start Archive or Stop Archive to control 20 | recording of the entire session. 21 |

22 |
23 | 27 |
28 | 29 |
30 |
31 | 32 |
33 |
Play an archive
34 |
35 |

36 | Click through to Past Archives to see examples of using the 37 | Archiving REST API to list archives showing status (started, 38 | stopped, available) and playback (for available archives). 39 |

40 |
41 | 44 |
45 | 46 |
47 |
48 | 49 |
50 | 51 |
52 | 53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /sample/Archiving/templates/participant.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | 5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 |

Participant

13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 |

Instructions

23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
WhenYou will see
Archiving is started
Archiving remains on
Archiving is stopped
47 |
48 |
49 |
50 | 51 | 56 | 57 | 58 | 59 | 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /sample/Archiving/web/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 | #subscribers div { 17 | float: left; 18 | } 19 | 20 | .bump-me { 21 | padding-top: 40px; 22 | } 23 | -------------------------------------------------------------------------------- /sample/Archiving/web/img/archiving-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/OpenTok-PHP-SDK/0e5b31608cb14664ce529321287032649f8c50bc/sample/Archiving/web/img/archiving-off.png -------------------------------------------------------------------------------- /sample/Archiving/web/img/archiving-on-idle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/OpenTok-PHP-SDK/0e5b31608cb14664ce529321287032649f8c50bc/sample/Archiving/web/img/archiving-on-idle.png -------------------------------------------------------------------------------- /sample/Archiving/web/img/archiving-on-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/OpenTok-PHP-SDK/0e5b31608cb14664ce529321287032649f8c50bc/sample/Archiving/web/img/archiving-on-message.png -------------------------------------------------------------------------------- /sample/Archiving/web/index.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/../templates', 39 | 'view' => new \Slim\Views\Twig(), 40 | )); 41 | 42 | // Intialize a cache, store it in the app container 43 | $app->container->singleton('cache', function () { 44 | return new Cache(); 45 | }); 46 | 47 | // Initialize OpenTok instance, store it in the app contianer 48 | $app->container->singleton('opentok', function () { 49 | return new OpenTok(getenv('API_KEY'), getenv('API_SECRET')); 50 | }); 51 | // Store the API Key in the app container 52 | $app->apiKey = getenv('API_KEY'); 53 | 54 | // If a sessionId has already been created, retrieve it from the cache 55 | $sessionId = $app->cache->getOrCreate('sessionId', array(), function () use ($app) { 56 | // If the sessionId hasn't been created, create it now and store it 57 | $session = $app->opentok->createSession(array( 58 | 'mediaMode' => MediaMode::ROUTED 59 | )); 60 | return $session->getSessionId(); 61 | }); 62 | 63 | // Configure routes 64 | $app->get('/', function () use ($app) { 65 | $app->render('index.html'); 66 | }); 67 | 68 | $app->get('/host', function () use ($app, $sessionId) { 69 | 70 | $token = $app->opentok->generateToken($sessionId, array( 71 | 'role' => Role::MODERATOR 72 | )); 73 | 74 | $app->render('host.html', array( 75 | 'apiKey' => $app->apiKey, 76 | 'sessionId' => $sessionId, 77 | 'token' => $token 78 | )); 79 | }); 80 | 81 | $app->get('/participant', function () use ($app, $sessionId) { 82 | 83 | $token = $app->opentok->generateToken($sessionId, array( 84 | 'role' => Role::MODERATOR 85 | )); 86 | 87 | $app->render('participant.html', array( 88 | 'apiKey' => $app->apiKey, 89 | 'sessionId' => $sessionId, 90 | 'token' => $token 91 | )); 92 | }); 93 | 94 | $app->get('/history', function () use ($app) { 95 | $page = intval($app->request->get('page')); 96 | if (empty($page)) { 97 | $page = 1; 98 | } 99 | 100 | $offset = ($page - 1) * 5; 101 | 102 | $archives = $app->opentok->listArchives($offset, 5); 103 | 104 | $toArray = function ($archive) { 105 | return $archive->toArray(); 106 | }; 107 | 108 | $app->render('history.html', array( 109 | 'archives' => array_map($toArray, $archives->getItems()), 110 | 'showPrevious' => $page > 1 ? '/history?page=' . ($page - 1) : null, 111 | 'showNext' => $archives->totalCount() > $offset + 5 ? '/history?page=' . ($page + 1) : null 112 | )); 113 | }); 114 | 115 | $app->get('/download/:archiveId', function ($archiveId) use ($app) { 116 | $archive = $app->opentok->getArchive($archiveId); 117 | $app->redirect($archive->url); 118 | }); 119 | 120 | $app->post('/start', function () use ($app, $sessionId) { 121 | 122 | $archive = $app->opentok->startArchive($sessionId, array( 123 | 'name' => "PHP Archiving Sample App", 124 | 'hasAudio' => ($app->request->post('hasAudio') == 'on'), 125 | 'hasVideo' => ($app->request->post('hasVideo') == 'on'), 126 | 'outputMode' => ($app->request->post('outputMode') == 'composed' ? OutputMode::COMPOSED : OutputMode::INDIVIDUAL) 127 | )); 128 | 129 | $app->response->headers->set('Content-Type', 'application/json'); 130 | echo $archive->toJson(); 131 | }); 132 | 133 | $app->get('/stop/:archiveId', function ($archiveId) use ($app) { 134 | $archive = $app->opentok->stopArchive($archiveId); 135 | $app->response->headers->set('Content-Type', 'application/json'); 136 | echo $archive->toJson(); 137 | }); 138 | 139 | $app->get('/delete/:archiveId', function ($archiveId) use ($app) { 140 | $app->opentok->deleteArchive($archiveId); 141 | $app->redirect('/history'); 142 | }); 143 | 144 | $app->run(); 145 | -------------------------------------------------------------------------------- /sample/Archiving/web/js/host.js: -------------------------------------------------------------------------------- 1 | var session = OT.initSession(apiKey, sessionId), 2 | publisher = OT.initPublisher('publisher'), 3 | archiveID = null; 4 | 5 | session.connect(token, function(error) { 6 | if (error) { 7 | console.error('Failed to connect', error); 8 | } else { 9 | session.publish(publisher, function(error) { 10 | if (error) { 11 | console.error('Failed to publish', error); 12 | } 13 | }); 14 | } 15 | }); 16 | 17 | session.on('streamCreated', function(event) { 18 | session.subscribe(event.stream, 'subscribers', { 19 | insertMode: 'append' 20 | }, function(error) { 21 | if (error) { 22 | console.error('Failed to subscribe', error); 23 | } 24 | }); 25 | }); 26 | 27 | session.on('archiveStarted', function(event) { 28 | archiveID = event.id; 29 | console.log('ARCHIVE STARTED'); 30 | $('.start').hide(); 31 | $('.stop').show(); 32 | disableForm(); 33 | }); 34 | 35 | session.on('archiveStopped', function(event) { 36 | archiveID = null; 37 | console.log('ARCHIVE STOPPED'); 38 | $('.start').show(); 39 | $('.stop').hide(); 40 | enableForm(); 41 | }); 42 | 43 | $(document).ready(function() { 44 | $('.start').click(function(event) { 45 | var options = $('.archive-options').serialize(); 46 | disableForm(); 47 | $.post('/start', options).fail(enableForm); 48 | }).show(); 49 | $('.stop').click(function(event){ 50 | $.get('stop/' + archiveID); 51 | }).hide(); 52 | }); 53 | 54 | 55 | function disableForm() { 56 | $('.archive-options-fields').attr('disabled', 'disabled'); 57 | } 58 | 59 | function enableForm() { 60 | $('.archive-options-fields').removeAttr('disabled'); 61 | } 62 | -------------------------------------------------------------------------------- /sample/Archiving/web/js/participant.js: -------------------------------------------------------------------------------- 1 | var session = OT.initSession(apiKey, sessionId), 2 | publisher = OT.initPublisher('publisher'); 3 | 4 | session.connect(token, function(error) { 5 | if (error) { 6 | console.error('Failed to connect', error); 7 | } else { 8 | session.publish(publisher, function(error) { 9 | if (error) { 10 | console.error('Failed to publish', error); 11 | } 12 | }); 13 | } 14 | }); 15 | 16 | session.on('streamCreated', function(event) { 17 | session.subscribe(event.stream, 'subscribers', { 18 | insertMode : 'append' 19 | }, function(error) { 20 | if (error) { 21 | console.error('Failed to subscribe', error); 22 | } 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /sample/HelloWorld/.gitignore: -------------------------------------------------------------------------------- 1 | .htaccess 2 | web/cache/ -------------------------------------------------------------------------------- /sample/HelloWorld/README.md: -------------------------------------------------------------------------------- 1 | # OpenTok Hello World PHP 2 | 3 | This is a simple demo app that shows how you can use the OpenTok-PHP-SDK to create Sessions, 4 | generate Tokens with those Sessions, and then pass these values to a JavaScript client that can 5 | connect and conduct a group chat. 6 | 7 | ## Running the App 8 | 9 | First, download the dependencies using [Composer](http://getcomposer.org) in this directory as well 10 | as the root SDK directory 11 | 12 | ``` 13 | $ cd ../../ 14 | $ composer.phar install 15 | $ cd sample/HelloWorld 16 | $ ../../composer.phar install 17 | ``` 18 | 19 | Next, input your own API Key and API Secret into the `run-demo` script file: 20 | 21 | ``` 22 | export API_KEY=0000000 23 | export API_SECRET=abcdef1234567890abcdef01234567890abcdef 24 | ``` 25 | 26 | Finally, start the PHP CLI development server (requires PHP >= 5.4) using the `run-demo` script 27 | 28 | ``` 29 | $ ./run-demo 30 | ``` 31 | 32 | Visit in your browser. Open it again in a second window. Smile! You've just 33 | set up a group chat. 34 | 35 | ## Walkthrough 36 | 37 | This demo application uses the [Slim PHP micro-framework](http://www.slimframework.com/) and 38 | a [lightweight caching library](https://github.com/Gregwar/Cache). These are similar to many other 39 | popular web frameworks and data caching/storage software. These concepts won't be explained but can 40 | be explore further at each of the websites linked above. 41 | 42 | ### Main Controller (web/index.php) 43 | 44 | The first thing done in this file is to require the autoloaders which pulls in all the dependencies 45 | that were installed by Composer. We now have the Slim framework, the Cache library, and most 46 | importantly the OpenTok SDK available. 47 | 48 | ```php 49 | require($autoloader); 50 | require($sdkAutoloader); 51 | 52 | use Slim\Slim; 53 | use Gregwar\Cache\Cache; 54 | 55 | use OpenTok\OpenTok; 56 | ``` 57 | 58 | Next the controller performs some basic checks on the environment, initializes the Slim application 59 | (`$app`), and sets up the cache to be stored in the application's container (`$app->cache`). 60 | 61 | The first thing that we do with OpenTok is to initialize an instance and also store it in the 62 | application container. At the same time, we also store the apiKey separately so that we can access 63 | it on its own. Notice that we needed to get the `API_KEY` and `API_SECRET` from the environment 64 | variables. 65 | 66 | ```php 67 | // Initialize OpenTok instance, store it in the app contianer 68 | $app->container->singleton('opentok', function () { 69 | return new OpenTok(getenv('API_KEY'), getenv('API_SECRET')); 70 | }); 71 | // Store the API Key in the app container 72 | $app->apiKey = getenv('API_KEY'); 73 | ``` 74 | 75 | Now we are ready to configure some routes. We only need one GET route for the root path because this 76 | application only has one page. Inside the route handler, we query the cache to see if we have stored 77 | a `sessionId` previously. The reason we use a cache in this application is because we want to generate 78 | a session only once, no matter how many times the page is loaded, so that all visitors can join the 79 | same OpenTok Session. In other applications, it would be common to save the `sessionId` in a database 80 | table. If the cache does not have a `sessionId` stored, like on the first run of the application, we 81 | use the stored OpenTok instance to create a Session. When we return its `sessionId`, that will be 82 | stored in the cache for later use. 83 | 84 | **NOTE:** in order to clear the cache, just delete the cache folder created in your demo app directory. 85 | 86 | ```php 87 | // If a sessionId has already been created, retrieve it from the cache 88 | $sessionId = $app->cache->getOrCreate('sessionId', array(), function () use ($app) { 89 | // If the sessionId hasn't been created, create it now and store it 90 | $session = $app->opentok->createSession(); 91 | return $session->getSessionId(); 92 | }); 93 | ``` 94 | 95 | Next inside the route handler, we generate a Token, so the client has permission to connect to that 96 | Session. This is again done by accessing the stored OpenTok instance. Since the token is not cached, 97 | a fresh one is generated each time. 98 | 99 | ```php 100 | // Generate a fresh token for this client 101 | $token = $app->opentok->generateToken($sessionId); 102 | ``` 103 | 104 | Lastly, we load a template called `helloworld.php` in the `templates/` directory, and pass in the 105 | three values needed for a client to connect to a Session: `apiKey`, `sessionId`, and `token`. 106 | 107 | ```php 108 | $app->render('helloworld.php', array( 109 | 'apiKey' => $app->apiKey, 110 | 'sessionId' => $sessionId, 111 | 'token' => $token 112 | )); 113 | ``` 114 | 115 | ### Main Template (templates/helloworld.php) 116 | 117 | This file simply sets up the HTML page for the JavaScript application to run, imports the 118 | JavaScript library, and passes the values created by the server into the JavaScript application 119 | inside `web/js/helloworld.js` 120 | 121 | ### JavaScript Application (web/js/helloworld.js) 122 | 123 | The group chat is mostly implemented in this file. At a high level, we connect to the given 124 | Session, publish a stream from our webcam, and listen for new streams from other clients to 125 | subscribe to. 126 | 127 | For more details, read the comments in the file or go to the 128 | [JavaScript Client Library](http://tokbox.com/opentok/libraries/client/js/) for a full reference. 129 | -------------------------------------------------------------------------------- /sample/HelloWorld/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "slim/slim": "2.*", 4 | "gregwar/cache": "1.0.*" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /sample/HelloWorld/run-demo: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$API_KEY" ] || [ -z "$API_SECRET" ] 4 | then 5 | export API_KEY= 6 | export API_SECRET= 7 | fi 8 | 9 | if [ -d "cache" ] 10 | then 11 | rm -rf cache/ 12 | fi 13 | 14 | php -S localhost:8080 -t web/ 15 | -------------------------------------------------------------------------------- /sample/HelloWorld/run-demo.bat: -------------------------------------------------------------------------------- 1 | :: Why? because windows can't do an OR within the conditional 2 | IF NOT DEFINED API_KEY GOTO defkeysecret 3 | IF NOT DEFINED API_SECRET GOTO defkeysecret 4 | GOTO skipdef 5 | 6 | :defkeysecret 7 | 8 | SET API_KEY= 9 | SET API_SECRET= 10 | 11 | :skipdef 12 | 13 | RD /q /s cache 14 | 15 | php.exe -S localhost:8080 -t web web/index.php 16 | -------------------------------------------------------------------------------- /sample/HelloWorld/templates/helloworld.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OpenTok Hello World 6 | 7 | 12 | 13 | 14 | 15 |

Hello, World!

16 | 17 |
18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /sample/HelloWorld/web/index.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/../templates' 36 | )); 37 | 38 | // Intialize a cache, store it in the app container 39 | $app->container->singleton('cache', function () { 40 | return new Cache(); 41 | }); 42 | 43 | // Initialize OpenTok instance, store it in the app contianer 44 | $app->container->singleton('opentok', function () { 45 | return new OpenTok(getenv('API_KEY'), getenv('API_SECRET')); 46 | }); 47 | // Store the API Key in the app container 48 | $app->apiKey = getenv('API_KEY'); 49 | 50 | // Configure routes 51 | $app->get('/', function () use ($app) { 52 | 53 | // If a sessionId has already been created, retrieve it from the cache 54 | $sessionId = $app->cache->getOrCreate('sessionId', array(), function () use ($app) { 55 | // If the sessionId hasn't been created, create it now and store it 56 | $session = $app->opentok->createSession(); 57 | return $session->getSessionId(); 58 | }); 59 | 60 | // Generate a fresh token for this client 61 | $token = $app->opentok->generateToken($sessionId); 62 | 63 | $app->render('helloworld.php', array( 64 | 'apiKey' => $app->apiKey, 65 | 'sessionId' => $sessionId, 66 | 'token' => $token 67 | )); 68 | }); 69 | 70 | $app->run(); 71 | -------------------------------------------------------------------------------- /sample/HelloWorld/web/js/helloworld.js: -------------------------------------------------------------------------------- 1 | // Initialize an OpenTok Session object. 2 | var session = OT.initSession(apiKey, sessionId); 3 | 4 | // Initialize a Publisher, and place it into the 'publisher' DOM element. 5 | var publisher = OT.initPublisher('publisher'); 6 | 7 | session.on('streamCreated', function(event) { 8 | // Called when another client publishes a stream. 9 | // Subscribe to the stream that caused this event. 10 | session.subscribe(event.stream, 'subscribers', { 11 | insertMode: 'append' 12 | }, function(error) { 13 | if (error) { 14 | console.error('Failed to subscribe', error); 15 | } 16 | }); 17 | }); 18 | 19 | // Connect to the session using your OpenTok API key and the client's token for the session 20 | session.connect(token, function(error) { 21 | if (error) { 22 | console.error('Failed to connect', error); 23 | } else { 24 | // Publish a stream, using the Publisher we initialzed earlier. 25 | // This triggers a streamCreated event on other clients. 26 | session.publish(publisher, function(error) { 27 | if (error) { 28 | console.error('Failed to publish', error); 29 | } 30 | }); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /sample/SipCall/.gitignore: -------------------------------------------------------------------------------- 1 | .htaccess 2 | web/cache/ -------------------------------------------------------------------------------- /sample/SipCall/README.md: -------------------------------------------------------------------------------- 1 | # OpenTok Hello SIP PHP 2 | 3 | This is a simple demo app that shows how you can use the OpenTok-PHP-SDK to join a SIP call. 4 | 5 | ## Running the App 6 | 7 | First, download the dependencies using [Composer](http://getcomposer.org) in this directory, as well 8 | as the root SDK directory 9 | 10 | ``` 11 | $ cd ../../ 12 | $ composer.phar install 13 | $ cd sample/SipCall 14 | $ ../../composer.phar install 15 | ``` 16 | 17 | Next, input your own API Key, API Secret, and SIP configuration into the `run-demo` script file: 18 | 19 | ``` 20 | export API_KEY=0000000 21 | export API_SECRET=abcdef1234567890abcdef01234567890abcdef 22 | export SIP_URI=sip: 23 | export SIP_USERNAME= 24 | export SIP_PASSWORD= 25 | export SIP_SECURE=false 26 | export SIP_FROM=003456@yourcompany.com 27 | ``` 28 | 29 | Finally, start the PHP CLI development server (requires PHP >= 5.4) using the `run-demo` script 30 | 31 | ``` 32 | $ ./run-demo 33 | ``` 34 | 35 | Visit in your browser. 36 | 37 | ## Walkthrough 38 | 39 | This demo application uses the same frameworks and libraries as the HelloWorld sample. If you have 40 | not already gotten familiar with the code in that project, consider doing so before continuing. 41 | 42 | ### Main Controller (web/index.php) 43 | 44 | This serves `templates/index.php` and passes in a generated session and token. It also provides an 45 | endpoing `/sip/start` which will dial into a SIP endpoint. 46 | 47 | ### Main Template (templates/index.php) 48 | 49 | This file simply sets up the HTML page for the JavaScript application to run, imports the 50 | JavaScript library, and passes the values created by the server into the JavaScript application 51 | inside `web/js/index.js` 52 | 53 | ### JavaScript Application (web/js/index.js) 54 | 55 | The group chat is mostly implemented in this file. It also implements a button to send a POST 56 | request to `/sip/start`, and another button to force disconnect all running SIP calls in the session. 57 | 58 | For more details, read the comments in the file or go to the 59 | [JavaScript Client Library](http://tokbox.com/opentok/libraries/client/js/) for a full reference. 60 | -------------------------------------------------------------------------------- /sample/SipCall/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "slim/slim": "2.*", 4 | "gregwar/cache": "1.0.*" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /sample/SipCall/run-demo: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$API_KEY" ] || [ -z "$API_SECRET" ] 4 | then 5 | # OpenTok Project Configuration (find these at https://tokbox.com/account) 6 | export API_KEY= 7 | export API_SECRET= 8 | 9 | # SIP Destination Configuration (find these with your SIP server provider) 10 | export SIP_URI=sip: 11 | export SIP_USERNAME= 12 | export SIP_PASSWORD= 13 | export SIP_SECURE=false 14 | 15 | # SIP from (optional) 16 | export SIP_FROM=003456@yourcompany.com 17 | fi 18 | 19 | if [ -d "cache" ] 20 | then 21 | rm -rf cache/ 22 | fi 23 | 24 | php -S localhost:8080 -t web/ 25 | -------------------------------------------------------------------------------- /sample/SipCall/run-demo.bat: -------------------------------------------------------------------------------- 1 | :: Why? because windows can't do an OR within the conditional 2 | IF NOT DEFINED API_KEY GOTO defkeysecret 3 | IF NOT DEFINED API_SECRET GOTO defkeysecret 4 | GOTO skipdef 5 | 6 | :defkeysecret 7 | 8 | :: OpenTok Project Configuration (find these at https://tokbox.com/account) 9 | SET API_KEY= 10 | SET API_SECRET= 11 | 12 | :: SIP Destination Configuration (find these with your SIP server provider) 13 | SET SIP_URI= 14 | SET SIP_USERNAME= 15 | SET SIP_PASSWORD= 16 | SET SIP_SECURE=false 17 | 18 | :: SIP from (optional) 19 | SET SIP_FROM= 20 | 21 | :skipdef 22 | 23 | RD /q /s cache 24 | 25 | php.exe -S localhost:8080 -t web web/index.php 26 | -------------------------------------------------------------------------------- /sample/SipCall/templates/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Wormhole Sample App 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 |
18 |
19 |

SIP Interconnect

20 |

Test OpenTok's SIP Interconnect API

21 |
22 | 23 |
24 |

Your Publisher

25 |
26 | 27 |
28 | Start SIP Call 29 | End SIP Calls 30 |
31 |
32 | 33 |
34 |

WebRTC Streams

35 |
36 |
37 |

SIP Streams

38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /sample/SipCall/web/index.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/../templates' 37 | )); 38 | 39 | // Intialize a cache, store it in the app container 40 | $app->container->singleton('cache', function () { 41 | return new Cache(); 42 | }); 43 | 44 | // Initialize OpenTok instance, store it in the app contianer 45 | $app->container->singleton('opentok', function () { 46 | return new OpenTok(getenv('API_KEY'), getenv('API_SECRET')); 47 | }); 48 | // Store the API Key in the app container 49 | $app->apiKey = getenv('API_KEY'); 50 | 51 | $app->sip = array( 52 | 'uri' => getenv('SIP_URI'), 53 | 'username' => getenv('SIP_USERNAME'), 54 | 'password' => getenv('SIP_PASSWORD'), 55 | 'secure' => (getenv('SIP_SECURE') === 'true'), 56 | 'from' => getenv('SIP_FROM'), 57 | ); 58 | 59 | // Configure routes 60 | $app->get('/', function () use ($app) { 61 | // If a sessionId has already been created, retrieve it from the cache 62 | $sessionId = $app->cache->getOrCreate('sessionId', array(), function () use ($app) { 63 | // If the sessionId hasn't been created, create it now and store it 64 | $session = $app->opentok->createSession(array('mediaMode' => MediaMode::ROUTED)); 65 | return $session->getSessionId(); 66 | }); 67 | 68 | // Generate a fresh token for this client 69 | $token = $app->opentok->generateToken($sessionId, array('role' => 'moderator')); 70 | 71 | $app->render('index.php', array( 72 | 'apiKey' => $app->apiKey, 73 | 'sessionId' => $sessionId, 74 | 'token' => $token 75 | )); 76 | }); 77 | 78 | $app->post('/sip/start', function () use ($app) { 79 | $sessionId = $app->request->post('sessionId'); 80 | 81 | // generate a token 82 | $token = $app->opentok->generateToken($sessionId, array('data' => 'sip=true')); 83 | 84 | // create the options parameter 85 | $options = array( 86 | 'secure' => $app->sip['secure'], 87 | 'from' => $app->sip['from'], 88 | ); 89 | if ($app->sip['username'] !== false) { 90 | $options['auth'] = array('username' => $app->sip['username'], 'password' => $app->sip['password']); 91 | } 92 | 93 | // make the sip call 94 | $sipCall = $app->opentok->dial($sessionId, $token, $app->sip['uri'], $options); 95 | 96 | echo $sipCall->toJson(); 97 | }); 98 | 99 | $app->run(); 100 | -------------------------------------------------------------------------------- /sample/SipCall/web/js/index.js: -------------------------------------------------------------------------------- 1 | var session = OT.initSession(apiKey, sessionId); 2 | session.on('streamCreated', function(event) { 3 | var tokenData = event.stream.connection.data; 4 | if (tokenData && tokenData.includes('sip=true')) { 5 | var element = 'sipPublisherContainer'; 6 | } else { 7 | var element = 'webrtcPublisherContainer'; 8 | } 9 | session.subscribe(event.stream, element, { 10 | insertMode: 'append' 11 | }, function(error) { 12 | if (error) { 13 | console.error('Failed to subscribe', error); 14 | } 15 | }); 16 | }) 17 | .connect(token, function(error) { 18 | if (error) { 19 | console.error('Failed to connect', error); 20 | return; 21 | } 22 | session.publish('selfPublisherContainer', { 23 | insertMode: 'append', 24 | height: '120px', 25 | width: '160px' 26 | }, function(error) { 27 | if (error) { 28 | console.error('Failed to publish', error); 29 | } 30 | }); 31 | }); 32 | $('#startSip').click(function(event) { 33 | $.post('/sip/start', {sessionId: sessionId, apiKey: apiKey}) 34 | .fail(function() { 35 | console.log('Failed to start SIP call - sample app server returned error.'); 36 | }); 37 | }); 38 | $('#stopSip').click(function(event) { 39 | OT.subscribers.where().forEach(function(subscriber) { 40 | var connection = subscriber.stream.connection; 41 | if (connection.data && connection.data.includes('sip=true')) { 42 | session.forceDisconnect(connection.connectionId); 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /sample/SipCall/web/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background-color: #efefef; 3 | } 4 | 5 | #main-nav .bottom { 6 | background-color: #303030 7 | height: 59px; 8 | } 9 | 10 | #main-nav .bottom .logo img { 11 | width: 113px; 12 | height: 30px; 13 | margin-top: 16px; 14 | } 15 | 16 | .main-header { 17 | display: inline-block; 18 | position: relative; 19 | width: 100%; 20 | padding: 17px 208px 0px 208px; 21 | box-sizing: border-box; 22 | } 23 | 24 | @media (max-width: 1024px) { 25 | .main-header { 26 | padding-left: 0; 27 | padding-right: 0; 28 | } 29 | } 30 | 31 | @media (max-width: 1128px) { 32 | .main-header { 33 | padding-left: 52px; 34 | padding-right: 52px; 35 | } 36 | } 37 | 38 | @media (max-width: 1232px) { 39 | .main-header { 40 | padding-left: 104px; 41 | padding-right: 104px; 42 | } 43 | } 44 | 45 | .main-header > header { 46 | margin: 0 auto; 47 | margin-bottom: 24px; 48 | border-radius: 8px; 49 | background: linear-gradient(to bottom, rgba(2,167,211,1) 0%, rgba(5,140,197,1) 100%); 50 | border: 1px solid #008ac4; 51 | max-width: 1024px; 52 | padding: 21px 29px; 53 | box-sizing: border-box; 54 | } 55 | 56 | .main-header > header h1 { 57 | font-family: Muli; 58 | font-weight: 500; 59 | font-size: 32px; 60 | line-height: 41px; 61 | margin: 0 0 9px 0; 62 | color: #FFF; 63 | } 64 | 65 | .main-header > header h1 sup { 66 | font-size: 14px; 67 | color: #9ce6ff; 68 | padding-left: 10px; 69 | vertical-align: top; 70 | } 71 | 72 | .main-header > header hr { 73 | opacity: 0.3; 74 | border: 0; 75 | border-bottom-width: 1px; 76 | border-bottom-color: #ffffff; 77 | border-bottom-style: solid; 78 | width: 100%; 79 | margin-top: 18px; 80 | margin-bottom: 16px; 81 | } 82 | 83 | 84 | .main-header > header h3 { 85 | font-family: Muli; 86 | font-weight: 400; 87 | font-size: 14px; 88 | line-height: 17px; 89 | letter-spacing: 0.3px; 90 | margin: 0; 91 | color: #82d0e8; 92 | } 93 | 94 | .main-header > section { 95 | max-width: 100%; 96 | padding: 21px 29px; 97 | box-sizing: border-box; 98 | } 99 | 100 | .main-container { 101 | display: block; 102 | padding: 21px 29px; 103 | box-sizing: border-box; 104 | position: relative; 105 | } 106 | 107 | #sip-controls { 108 | margin: 0 auto 20px auto; 109 | text-align: center; 110 | } 111 | 112 | #sipPublisherContainer, #webrtcPublisherContainer { 113 | height: 240px; 114 | background-color: #DDD; 115 | position: relative; 116 | margin: 0px 20px 0px 20px; 117 | border-radius: 8px; 118 | } 119 | 120 | #sipPublisherContainer > *, #webrtcPublisherContainer > * { 121 | transition-property: all; 122 | transition-duration: 0.5s; 123 | display: inline-block; 124 | } 125 | 126 | .streams h3 { 127 | margin-left: 20px; 128 | } 129 | 130 | #selfPublisherContainer { 131 | position: absolute; 132 | top: 0px; 133 | right: 0px; 134 | } 135 | 136 | #selfPublisherContainer h3 { 137 | margin-right: 20px; 138 | } 139 | -------------------------------------------------------------------------------- /src/OpenTok/Archive.php: -------------------------------------------------------------------------------- 1 | true) or not (false). 21 | * 22 | * @property bool $hasAudio 23 | * Whether the archive has an audio track (true) or not (false). 24 | * 25 | * @property string $id 26 | * The archive ID. 27 | * 28 | * @property string $name 29 | * The name of the archive. If no name was provided when the archive was created, this is set 30 | * to null. 31 | * 32 | * @property string $outputMode 33 | * The name of the archive. If no name was provided when the archive was created, this is set 34 | * to null. 35 | * 36 | * @property string $partnerId 37 | * The API key associated with the archive. 38 | * 39 | * @property string $reason 40 | * For archives with the status "stopped" or "failed", this string describes the reason 41 | * the archive stopped (such as "maximum duration exceeded") or failed. 42 | * 43 | * @property string $resolution 44 | * The resolution of the archive, either "640x480" (SD landscape, the default), "1280x720" (HD landscape), 45 | * "1920x1080" (FHD landscape), "480x640" (SD portrait), "720x1280" (HD portrait), or "1080x1920" (FHD portrait). 46 | * You may want to use a portrait aspect ratio for archives that include video streams from mobile devices (which often use the portrait aspect ratio). 47 | * 48 | * @property string $sessionId 49 | * The session ID of the OpenTok session associated with this archive. 50 | * 51 | * @property string $multiArchiveTag 52 | * Whether Multiple Archive is switched on, which will be a unique string for each simultaneous archive of an ongoing session. 53 | * See https://tokbox.com/developer/guides/archiving/#simultaneous-archives for more information. 54 | * 55 | * @property string $size 56 | * The size of the MP4 file. For archives that have not been generated, this value is set to 0. 57 | * 58 | * @property string $streamMode 59 | * Whether streams included in the archive are selected automatically (StreamMode.AUTO) or 60 | * manually (StreamMode.MANUAL). When streams are selected automatically (StreamMode.AUTO), 61 | * all streams in the session can be included in the archive. When streams are selected manually 62 | * (StreamMode.MANUAL), you specify streams to be included based on calls to the 63 | * Archive.addStreamToArchive() and Archive.removeStreamFromArchive() methods. 64 | * With manual mode, you can specify whether a stream's audio, video, or both are included in the 65 | * archive. In both automatic and manual modes, the archive composer includes streams based on 66 | * stream 67 | * prioritization rules. 68 | * 69 | * @property string $status 70 | * The status of the archive, which can be one of the following: 71 | * 72 | *
    73 | *
  • "available" -- The archive is available for download from the OpenTok cloud.
  • 74 | *
  • "expired" -- The archive is no longer available for download from the OpenTok 75 | * cloud.
  • 76 | *
  • "failed" -- The archive recording failed.
  • 77 | *
  • "paused" -- The archive is in progress and no clients are publishing streams to 78 | * the session. When an archive is in progress and any client publishes a stream, 79 | * the status is "started". When an archive is "paused", nothing is recorded. When 80 | * a client starts publishing a stream, the recording starts (or resumes). If all clients 81 | * disconnect from a session that is being archived, the status changes to "paused", and 82 | * after 60 seconds the archive recording stops (and the status changes to "stopped").
  • 83 | *
  • "started" -- The archive started and is in the process of being recorded.
  • 84 | *
  • "stopped" -- The archive stopped recording.
  • 85 | *
  • "uploaded" -- The archive is available for download from the the upload target 86 | * Amazon S3 bucket or Windows Azure container you set up for your 87 | * OpenTok project.
  • 88 | *
89 | * 90 | * @property string $url 91 | * The download URL of the available MP4 file. This is only set for an archive with the status set to 92 | * "available"; for other archives, (including archives with the status "uploaded") this property is 93 | * set to null. The download URL is obfuscated, and the file is only available from the URL for 94 | * 10 minutes. To generate a new URL, call the Archive.listArchives() or OpenTok.getArchive() method. 95 | */ 96 | class Archive 97 | { 98 | // NOTE: after PHP 5.3.0 support is dropped, the class can implement JsonSerializable 99 | 100 | /** @internal */ 101 | private $data; 102 | /** @internal */ 103 | private $isDeleted; 104 | /** @internal */ 105 | private $client; 106 | /** @internal */ 107 | private $multiArchiveTag; 108 | /** 109 | * @var mixed|null 110 | */ 111 | private const PERMITTED_AUTO_RESOLUTIONS = [ 112 | '480x640', 113 | "640x480", 114 | "720x1280", 115 | "1280x720", 116 | "1080x1920", 117 | "1920x1080" 118 | ]; 119 | 120 | /** @internal */ 121 | public function __construct($archiveData, $options = array()) 122 | { 123 | // unpack optional arguments (merging with default values) into named variables 124 | $defaults = array( 125 | 'apiKey' => null, 126 | 'apiSecret' => null, 127 | 'apiUrl' => 'https://api.opentok.com', 128 | 'client' => null, 129 | 'streamMode' => StreamMode::AUTO 130 | ); 131 | $options = array_merge($defaults, array_intersect_key($options, $defaults)); 132 | list($apiKey, $apiSecret, $apiUrl, $client, $streamMode) = array_values($options); 133 | 134 | // validate params 135 | Validators::validateArchiveData($archiveData); 136 | Validators::validateClient($client); 137 | Validators::validateHasStreamMode($streamMode); 138 | 139 | if (isset($archiveData['maxBitrate']) && isset($archiveData['quantizationParameter'])) { 140 | throw new \DomainException('Max Bitrate cannot be set with QuantizationParameter '); 141 | } 142 | 143 | $this->data = $archiveData; 144 | 145 | if (isset($this->data['multiArchiveTag'])) { 146 | $this->multiArchiveTag = $this->data['multiArchiveTag']; 147 | } 148 | 149 | $this->client = isset($client) ? $client : new Client(); 150 | if (!$this->client->isConfigured()) { 151 | Validators::validateApiUrl($apiUrl); 152 | 153 | $this->client->configure($apiKey, $apiSecret, $apiUrl); 154 | } 155 | } 156 | 157 | public static function getPermittedResolutions() 158 | { 159 | return self::PERMITTED_AUTO_RESOLUTIONS; 160 | } 161 | 162 | /** @internal */ 163 | public function __get($name) 164 | { 165 | if ($this->isDeleted) { 166 | // TODO: throw an logic error about not being able to stop an archive thats deleted 167 | } 168 | 169 | switch ($name) { 170 | case 'createdAt': 171 | case 'duration': 172 | case 'id': 173 | case 'name': 174 | case 'partnerId': 175 | case 'reason': 176 | case 'sessionId': 177 | case 'size': 178 | case 'status': 179 | case 'url': 180 | case 'hasVideo': 181 | case 'hasAudio': 182 | case 'outputMode': 183 | case 'resolution': 184 | case 'streamMode': 185 | case 'maxBitrate': 186 | case 'quantizationParameter': 187 | return $this->data[$name]; 188 | case 'multiArchiveTag': 189 | return $this->multiArchiveTag; 190 | default: 191 | return null; 192 | } 193 | } 194 | 195 | /** 196 | * Stops the OpenTok archive, if it is being recorded. 197 | *

198 | * Archives automatically stop recording after 120 minutes or when all clients have 199 | * disconnected from the session being archived. 200 | * 201 | * @throws Exception\ArchiveException The archive is not being recorded. 202 | */ 203 | public function stop() 204 | { 205 | if ($this->isDeleted) { 206 | // TODO: throw an logic error about not being able to stop an archive thats deleted 207 | } 208 | 209 | $archiveData = $this->client->stopArchive($this->data['id']); 210 | 211 | try { 212 | Validators::validateArchiveData($archiveData); 213 | } catch (InvalidArgumentException $e) { 214 | throw new ArchiveUnexpectedValueException('The archive JSON returned after stopping was not valid', null, $e); 215 | } 216 | 217 | $this->data = $archiveData; 218 | return $this; 219 | } 220 | 221 | /** 222 | * Deletes an OpenTok archive. 223 | *

224 | * You can only delete an archive which has a status of "available", "uploaded", or "deleted". 225 | * Deleting an archive removes its record from the list of archives. For an "available" archive, 226 | * it also removes the archive file, making it unavailable for download. For a "deleted" 227 | * archive, the archive remains deleted. 228 | * 229 | * @throws Exception\ArchiveException There archive status is not "available", "updated", 230 | * or "deleted". 231 | */ 232 | public function delete() 233 | { 234 | if ($this->isDeleted) { 235 | // TODO: throw an logic error about not being able to stop an archive thats deleted 236 | } 237 | 238 | if ($this->client->deleteArchive($this->data['id'])) { 239 | $this->data = array(); 240 | $this->isDeleted = true; 241 | return true; 242 | } 243 | return false; 244 | } 245 | 246 | /** 247 | * Returns a JSON representation of this Archive object. 248 | */ 249 | public function toJson() 250 | { 251 | return json_encode($this->jsonSerialize()); 252 | } 253 | 254 | /** 255 | * Adds a stream to a currently running archive that was started with the 256 | * the streamMode set to StreamMode.Manual. You can call the method 257 | * repeatedly with the same stream ID, to toggle the stream's audio or video in the archive. 258 | * 259 | * @param String $streamId The stream ID. 260 | * @param Boolean $hasAudio Whether the archive should include the stream's audio (true, the default) 261 | * or not (false). 262 | * @param Boolean $hasVideo Whether the archive should include the stream's video (true, the default) 263 | * or not (false). 264 | * 265 | * @return Boolean Returns true on success. 266 | */ 267 | public function addStreamToArchive(string $streamId, bool $hasAudio, bool $hasVideo): bool 268 | { 269 | if ($this->streamMode === StreamMode::AUTO) { 270 | throw new InvalidArgumentException('Cannot add stream to an Archive in auto stream mode'); 271 | } 272 | 273 | if ($hasAudio === false && $hasVideo === false) { 274 | throw new InvalidArgumentException('Both hasAudio and hasVideo cannot be false'); 275 | } 276 | 277 | if ($this->client->addStreamToArchive( 278 | $this->data['id'], 279 | $streamId, 280 | $hasVideo, 281 | $hasVideo 282 | )) { 283 | return true; 284 | } 285 | 286 | return false; 287 | } 288 | 289 | /** 290 | * Removes a stream from a currently running archive that was started with the 291 | * the streamMode set to StreamMode.Manual. 292 | * 293 | * @param String $streamId The stream ID. 294 | * 295 | * @return Boolean Returns true on success. 296 | */ 297 | public function removeStreamFromArchive(string $streamId): bool 298 | { 299 | if ($this->streamMode === StreamMode::AUTO) { 300 | throw new InvalidArgumentException('Cannot remove stream to an Archive in auto stream mode'); 301 | } 302 | 303 | if ($this->client->removeStreamFromArchive( 304 | $this->data['id'], 305 | $streamId 306 | )) { 307 | return true; 308 | } 309 | 310 | return false; 311 | } 312 | 313 | /** 314 | * Returns an associative array representation of this Archive object. 315 | * @deprecated 3.0.0 A more standard name for this method is supplied by JsonSerializable 316 | * @see Archive::jsonSerialize() for a method with the same behavior 317 | */ 318 | public function toArray() 319 | { 320 | return $this->data; 321 | } 322 | 323 | public function jsonSerialize() 324 | { 325 | return $this->data; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/OpenTok/ArchiveList.php: -------------------------------------------------------------------------------- 1 | null, 37 | 'apiSecret' => null, 38 | 'apiUrl' => 'https://api.opentok.com', 39 | 'client' => null 40 | ); 41 | $options = array_merge($defaults, array_intersect_key($options, $defaults)); 42 | list($apiKey, $apiSecret, $apiUrl, $client) = array_values($options); 43 | 44 | // validate params 45 | Validators::validateArchiveListData($archiveListData); 46 | Validators::validateClient($client); 47 | 48 | $this->data = $archiveListData; 49 | 50 | $this->client = isset($client) ? $client : new Client(); 51 | if (!$this->client->isConfigured()) { 52 | Validators::validateApiUrl($apiUrl); 53 | 54 | $this->client->configure($apiKey, $apiSecret, $apiUrl); 55 | } 56 | } 57 | 58 | /** 59 | * Returns the number of total archives for the API key. 60 | */ 61 | public function totalCount() 62 | { 63 | return $this->data['count']; 64 | } 65 | 66 | /** 67 | * Returns an array of Archive objects. 68 | * 69 | * @return Archive[] 70 | */ 71 | public function getItems() 72 | { 73 | if (!$this->items) { 74 | $items = array(); 75 | foreach ($this->data['items'] as $archiveData) { 76 | $items[] = new Archive($archiveData, array( 'client' => $this->client )); 77 | } 78 | $this->items = $items; 79 | } 80 | return $this->items; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/OpenTok/ArchiveMode.php: -------------------------------------------------------------------------------- 1 | createSession method and 9 | * the return value for the Session->getArchiveMode() method. 10 | *

11 | * See OpenTok->createSession() 12 | * and Session->getArchiveMode(). 13 | */ 14 | abstract class ArchiveMode extends BasicEnum 15 | { 16 | /** 17 | * The session is not archived automatically. To archive the session, you can call the 18 | * \OpenTok\OpenTok->startArchive() method. 19 | */ 20 | public const MANUAL = 'manual'; 21 | /** 22 | * The session is archived automatically (as soon as there are clients connected 23 | * to the session). 24 | */ 25 | public const ALWAYS = 'always'; 26 | } 27 | -------------------------------------------------------------------------------- /src/OpenTok/Broadcast.php: -------------------------------------------------------------------------------- 1 | OpenTok live streaming developer guide 32 | * for more information on how to use this URL. For each RTMP stream, the RTMP server URL and stream 33 | * name are provided, along with the RTMP stream's status. 34 | * 35 | * @property boolean $isStopped 36 | * Whether the broadcast is stopped (true) or in progress (false). 37 | * 38 | * @property string $multiBroadcastTag 39 | * Whether Multiple Broadcast is switched on, which will be a unique string for each simultaneous broadcast of an ongoing session. 40 | * See https://tokbox.com/developer/guides/archiving/#simultaneous-archives for more information. 41 | * 42 | * @property boolean $isHls 43 | * Whether the broadcast supports HLS. 44 | * 45 | * @property boolean $isDvr 46 | * Whether the broadcast supports DVR functionality for the HLS stream. 47 | 48 | * @property string $status 49 | * Broadcast state. Either `started` or `stopped` 50 | * 51 | * @property string $maxBitRate 52 | * Max Bitrate allowed for the broadcast composing. Must be between 400000 and 2000000 53 | * 54 | * @property boolean $isLowLatency 55 | * Whether the broadcast supports low-latency mode for the HLS stream. 56 | * 57 | * @property string $resolution 58 | * The resolution of the archive, either "640x480" (SD landscape, the default), "1280x720" (HD landscape), 59 | * "1920x1080" (FHD landscape), "480x640" (SD portrait), "720x1280" (HD portrait), or "1080x1920" (FHD portrait). 60 | * You may want to use a portrait aspect ratio for archives that include video streams from mobile devices (which often use the portrait aspect ratio). 61 | * 62 | * @property string $streamMode 63 | * Whether streams included in the broadcast are selected automatically (StreamMode.AUTO) 64 | * or manually (StreamMode.MANUAL). When streams are selected automatically (StreamMode.AUTO), 65 | * all streams in the session can be included in the broadcast. When streams are selected manually 66 | * (StreamMode.MANUAL), you specify streams to be included based on calls to the 67 | * Broadcast.addStreamToBroadcast() and Broadcast.removeStreamFromBroadcast() methods. 68 | * With manual mode, you can specify whether a stream's audio, video, or both are included in the 69 | * broadcast. In both automatic and manual modes, the broadcast composer includes streams based on 70 | * stream 71 | * prioritization rules. 72 | */ 73 | class Broadcast 74 | { 75 | /** @ignore */ 76 | private $data; 77 | /** @ignore */ 78 | private $isStopped; 79 | /** @ignore */ 80 | private $client; 81 | /** @ignore */ 82 | private $isHls; 83 | /** @ignore */ 84 | private $isLowLatency; 85 | /** @ignore */ 86 | private $isDvr; 87 | /** @ignore */ 88 | private $multiBroadcastTag; 89 | /** @ignore */ 90 | private $resolution; 91 | /** @ignore */ 92 | private $hasAudio; 93 | /** @ignore */ 94 | private $hasVideo; 95 | /** @ignore */ 96 | private $status; 97 | /** @ignore */ 98 | private $maxBitRate; 99 | 100 | public function __construct($broadcastData, $options = array()) 101 | { 102 | // unpack optional arguments (merging with default values) into named variables 103 | // when adding these properties like this, it's worth noting that the method that 104 | // starts a broadcast ALSO sets a load of defaults 105 | $defaults = array( 106 | 'apiKey' => null, 107 | 'apiSecret' => null, 108 | 'apiUrl' => 'https://api.opentok.com', 109 | 'client' => null, 110 | 'isStopped' => false, 111 | 'streamMode' => StreamMode::AUTO, 112 | 'isHls' => true, 113 | 'isLowLatency' => false, 114 | 'isDvr' => false, 115 | 'hasAudio' => true, 116 | 'hasVideo' => true 117 | ); 118 | 119 | $options = array_merge($defaults, array_intersect_key($options, $defaults)); 120 | 121 | Validators::validateBroadcastData($broadcastData); 122 | Validators::validateClient($options['client']); 123 | Validators::validateHasStreamMode($options['streamMode']); 124 | 125 | $this->data = $broadcastData; 126 | 127 | if (isset($this->data['multiBroadcastTag'])) { 128 | $this->multiBroadcastTag = $this->data['multiBroadcastTag']; 129 | } 130 | 131 | if (isset($this->data['maxBitRate'])) { 132 | $this->maxBitRate = $this->data['maxBitRate']; 133 | } 134 | 135 | if (isset($this->data['status'])) { 136 | $this->status = $this->data['status']; 137 | } 138 | 139 | $this->isStopped = $options['isStopped']; 140 | $this->resolution = $this->data['resolution']; 141 | $this->isHls = isset($this->data['settings']['hls']); 142 | $this->isLowLatency = $this->data['settings']['hls']['lowLatency'] ?? false; 143 | $this->isDvr = $this->data['settings']['hls']['dvr'] ?? false; 144 | $this->hasAudio = $options['hasAudio']; 145 | $this->hasVideo = $options['hasVideo']; 146 | 147 | $this->client = $options['client'] ?? new Client(); 148 | 149 | if (!$this->client->isConfigured()) { 150 | Validators::validateApiUrl($options['apiUrl']); 151 | 152 | $this->client->configure($options['apiKey'], $options['apiSecret'], $options['apiUrl']); 153 | } 154 | } 155 | 156 | /** @ignore */ 157 | public function __get($name) 158 | { 159 | switch ($name) { 160 | case 'createdAt': 161 | case 'updatedAt': 162 | case 'id': 163 | case 'partnerId': 164 | case 'sessionId': 165 | case 'broadcastUrls': 166 | case 'maxDuration': 167 | case 'streamMode': 168 | return $this->data[$name]; 169 | case 'resolution': 170 | return $this->resolution; 171 | case 'hlsUrl': 172 | return $this->data['broadcastUrls']['hls']; 173 | case 'isStopped': 174 | return $this->isStopped; 175 | case 'isHls': 176 | return $this->isHls; 177 | case 'isLowLatency': 178 | return $this->isLowLatency; 179 | case 'isDvr': 180 | return $this->isDvr; 181 | case 'multiBroadcastTag': 182 | return $this->multiBroadcastTag; 183 | case 'hasAudio': 184 | return $this->hasAudio; 185 | case 'hasVideo': 186 | return $this->hasVideo; 187 | case 'status': 188 | return $this->status; 189 | case 'maxBitRate': 190 | return $this->maxBitRate; 191 | default: 192 | return null; 193 | } 194 | } 195 | 196 | /** 197 | * Stops the broadcast. 198 | */ 199 | public function stop() 200 | { 201 | if ($this->isStopped) { 202 | throw new BroadcastDomainException( 203 | 'You cannot stop a broadcast which is already stopped.' 204 | ); 205 | } 206 | 207 | $broadcastData = $this->client->stopBroadcast($this->data['id']); 208 | 209 | try { 210 | Validators::validateBroadcastData($broadcastData); 211 | } catch (InvalidArgumentException $e) { 212 | throw new BroadcastUnexpectedValueException('The broadcast JSON returned after stopping was not valid', null, $e); 213 | } 214 | 215 | $this->data = $broadcastData; 216 | return $this; 217 | } 218 | 219 | // TODO: not yet implemented by the platform 220 | // public function getLayout() 221 | // { 222 | // $layoutData = $this->client->getLayout($this->id, 'broadcast'); 223 | // return Layout::fromData($layoutData); 224 | // } 225 | 226 | /** 227 | * Updates the layout of the broadcast. 228 | *

229 | * See Configuring 230 | * video layout for OpenTok live streaming broadcasts. 231 | * 232 | * @param Layout $layout An object defining the layout type for the broadcast. 233 | */ 234 | public function updateLayout($layout) 235 | { 236 | Validators::validateLayout($layout); 237 | 238 | // TODO: platform implementation did not meet API review spec 239 | // $layoutData = $this->client->updateLayout($this->id, $layout, 'broadcast'); 240 | // return Layout::fromData($layoutData); 241 | 242 | $this->client->updateLayout($this->id, $layout, 'broadcast'); 243 | } 244 | 245 | /** 246 | * Adds a stream to a currently running broadcast that was started with the 247 | * the streamMode set to StreamMode.Manual. You can call the method 248 | * repeatedly with the same stream ID, to toggle the stream's audio or video in the broadcast. 249 | * 250 | * @param String $streamId The stream ID. 251 | * @param Boolean $hasAudio Whether the broadcast should include the stream's audio (true, the default) 252 | * or not (false). 253 | * @param Boolean $hasVideo Whether the broadcast should include the stream's video (true, the default) 254 | * or not (false). 255 | * 256 | * @return Boolean Returns true on success. 257 | */ 258 | public function addStreamToBroadcast(string $streamId, bool $hasAudio, bool $hasVideo): bool 259 | { 260 | if ($this->streamMode === StreamMode::AUTO) { 261 | throw new InvalidArgumentException('Cannot add stream to a Broadcast in auto stream mode'); 262 | } 263 | 264 | if ($hasAudio === false && $hasVideo === false) { 265 | throw new InvalidArgumentException('Both hasAudio and hasVideo cannot be false'); 266 | } 267 | 268 | if ($this->client->addStreamToBroadcast( 269 | $this->data['id'], 270 | $streamId, 271 | $hasVideo, 272 | $hasVideo 273 | )) { 274 | return true; 275 | } 276 | 277 | return false; 278 | } 279 | 280 | /** 281 | * Removes a stream from a currently running broadcast that was started with the 282 | * the streamMode set to StreamMode.Manual. 283 | * 284 | * @param String $streamId The stream ID. 285 | * 286 | * @return Boolean Returns true on success. 287 | */ 288 | public function removeStreamFromBroadcast(string $streamId): bool 289 | { 290 | if ($this->streamMode === StreamMode::AUTO) { 291 | throw new InvalidArgumentException('Cannot remove stream from a Broadcast in auto stream mode'); 292 | } 293 | 294 | if ($this->client->removeStreamFromBroadcast( 295 | $this->data['id'], 296 | $streamId 297 | )) { 298 | return true; 299 | } 300 | 301 | return false; 302 | } 303 | 304 | public function jsonSerialize() 305 | { 306 | return $this->data; 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/OpenTok/Exception/ArchiveAuthenticationException.php: -------------------------------------------------------------------------------- 1 | setArchiveLayout() and OpenTok->updateBroadcastLayout() methods. 10 | *

11 | * To instantiate a Layout object, call one of the static methods of the Layout class: 12 | * getBestFit(), getPIP(), getVerticalPresentation(), 13 | * getHorizontalPresentation(), or createCustom(). 14 | *

15 | * See OpenTok->setArchiveLayout(), 16 | * OpenTok->updateBroadcastLayout(), 17 | * Customizing 18 | * the video layout for composed archives, and 19 | * Configuring 20 | * video layout for OpenTok live streaming broadcasts. 21 | */ 22 | class Layout implements \JsonSerializable 23 | { 24 | public const LAYOUT_BESTFIT = 'bestFit'; 25 | public const LAYOUT_CUSTOM = 'custom'; 26 | public const LAYOUT_HORIZONTAL = 'horizontalPresentation'; 27 | public const LAYOUT_PIP = 'pip'; 28 | public const LAYOUT_VERTICAL = 'verticalPresentation'; 29 | 30 | /** 31 | * Type of layout that we are sending 32 | * @var string 33 | * @ignore 34 | * */ 35 | private $type; 36 | 37 | /** 38 | * Type of layout to use for screen sharing 39 | * @var string 40 | * @ignore 41 | */ 42 | private $screenshareType; 43 | 44 | /** 45 | * Custom stylesheet if our type is 'custom' 46 | * @var string 47 | * @ignore 48 | */ 49 | private $stylesheet; 50 | 51 | /** @ignore */ 52 | private function __construct(string $type, ?string $stylesheet = null) 53 | { 54 | $this->type = $type; 55 | $this->stylesheet = $stylesheet; 56 | } 57 | 58 | /** 59 | * Returns a Layout object defining a custom layout type. 60 | * 61 | * @param array $options An array containing one property: $stylesheet, 62 | * which is a string containing the stylesheet to be used for the layout. 63 | */ 64 | public static function createCustom(array $options): Layout 65 | { 66 | // unpack optional arguments (merging with default values) into named variables 67 | // NOTE: the default value of stylesheet=null will not pass validation, this essentially 68 | // means that stylesheet is not optional. its still purposely left as part of the 69 | // $options argument so that it can become truly optional in the future. 70 | $defaults = ['stylesheet' => null]; 71 | $options = array_merge($defaults, array_intersect_key($options, $defaults)); 72 | list($stylesheet) = array_values($options); 73 | 74 | // validate arguments 75 | Validators::validateLayoutStylesheet($stylesheet); 76 | 77 | return new Layout(static::LAYOUT_CUSTOM, $stylesheet); 78 | } 79 | 80 | /** @ignore */ 81 | public static function fromData(array $layoutData): Layout 82 | { 83 | if (array_key_exists('stylesheet', $layoutData)) { 84 | return new Layout($layoutData['type'], $layoutData['stylesheet']); 85 | } 86 | 87 | return new Layout($layoutData['type']); 88 | } 89 | 90 | /** 91 | * Returns a Layout object defining the "best fit" predefined layout type. 92 | */ 93 | public static function getBestFit(): Layout 94 | { 95 | return new Layout(static::LAYOUT_BESTFIT); 96 | } 97 | 98 | /** 99 | * Returns a Layout object defining the "picture-in-picture" predefined layout type. 100 | */ 101 | public static function getPIP(): Layout 102 | { 103 | return new Layout(static::LAYOUT_PIP); 104 | } 105 | 106 | /** 107 | * Returns a Layout object defining the "vertical presentation" predefined layout type. 108 | */ 109 | public static function getVerticalPresentation(): Layout 110 | { 111 | return new Layout(static::LAYOUT_VERTICAL); 112 | } 113 | 114 | /** 115 | * Returns a Layout object defining the "horizontal presentation" predefined layout type. 116 | */ 117 | public static function getHorizontalPresentation(): Layout 118 | { 119 | return new Layout(static::LAYOUT_HORIZONTAL); 120 | } 121 | 122 | public function setScreenshareType(string $screenshareType): Layout 123 | { 124 | if ($this->type === Layout::LAYOUT_BESTFIT) { 125 | $layouts = [ 126 | Layout::LAYOUT_BESTFIT, 127 | Layout::LAYOUT_HORIZONTAL, 128 | Layout::LAYOUT_PIP, 129 | Layout::LAYOUT_VERTICAL 130 | ]; 131 | 132 | if (!in_array($screenshareType, $layouts)) { 133 | throw new \RuntimeException('Screenshare type must be of a valid layout type'); 134 | } 135 | 136 | $this->screenshareType = $screenshareType; 137 | return $this; 138 | } 139 | 140 | throw new \RuntimeException('Screenshare type cannot be set on a layout type other than bestFit'); 141 | } 142 | 143 | #[\ReturnTypeWillChange] 144 | public function jsonSerialize() 145 | { 146 | return $this->toArray(); 147 | } 148 | 149 | /** 150 | * Return a json-encoded string representation of the layout 151 | */ 152 | public function toJson(): string 153 | { 154 | return json_encode($this->jsonSerialize()); 155 | } 156 | 157 | public function toArray(): array 158 | { 159 | $data = array( 160 | 'type' => $this->type 161 | ); 162 | 163 | // omit 'stylesheet' property unless it is explicitly defined 164 | if (isset($this->stylesheet)) { 165 | $data['stylesheet'] = $this->stylesheet; 166 | } 167 | 168 | // omit 'screenshareType' property unless it is explicitly defined 169 | if (isset($this->screenshareType)) { 170 | $data['screenshareType'] = $this->screenshareType; 171 | } 172 | 173 | return $data; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/OpenTok/MediaMode.php: -------------------------------------------------------------------------------- 1 | createSession() 9 | * method. 10 | */ 11 | abstract class MediaMode extends BasicEnum 12 | { 13 | /** 14 | * The session will send streams using the OpenTok Media Router. 15 | */ 16 | const ROUTED = 'disabled'; 17 | /** 18 | * The session will attempt send streams directly between clients. If clients cannot connect 19 | * due to firewall restrictions, the session uses the OpenTok TURN server to relay streams. 20 | */ 21 | const RELAYED = 'enabled'; 22 | } 23 | -------------------------------------------------------------------------------- /src/OpenTok/OutputMode.php: -------------------------------------------------------------------------------- 1 | startArchive() method 9 | * and for the outputMode property of the Archive class. 10 | * 11 | * See OpenTok->startArchive() 12 | * and Archive.outputMode. 13 | */ 14 | abstract class OutputMode extends BasicEnum 15 | { 16 | /** 17 | * All streams in the archive are recorded to a single (composed) file. 18 | */ 19 | const COMPOSED = 'composed'; 20 | /** 21 | * Each stream in the archive is recorded to its own individual file. 22 | */ 23 | const INDIVIDUAL = 'individual'; 24 | } 25 | -------------------------------------------------------------------------------- /src/OpenTok/Render.php: -------------------------------------------------------------------------------- 1 | data = json_decode($data, true); 51 | } 52 | 53 | /** @internal */ 54 | public function __get($name) 55 | { 56 | switch ($name) { 57 | case 'id': 58 | case 'sessionId': 59 | case 'projectId': 60 | case 'createdAt': 61 | case 'updatedAt': 62 | case 'url': 63 | case 'resolution': 64 | case 'status': 65 | case 'streamId': 66 | return $this->data[$name]; 67 | default: 68 | return null; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/OpenTok/Role.php: -------------------------------------------------------------------------------- 1 | generateToken() 9 | * method. 10 | */ 11 | abstract class Role extends BasicEnum 12 | { 13 | /** 14 | * A subscriber can only subscribe to streams. 15 | */ 16 | const SUBSCRIBER = 'subscriber'; 17 | /** 18 | * A publisher can publish streams, subscribe to streams, and signal. (This is the default 19 | * value if you do not set a role.) 20 | */ 21 | const PUBLISHER = 'publisher'; 22 | /** 23 | * In addition to the privileges granted to a publisher, a moderator can perform 24 | * moderation functions, such as forcing clients to disconnect, to stop publishing streams, 25 | * or to mute audio in published streams. 26 | * 27 | * See the 28 | * Moderation developer guide. 29 | */ 30 | const MODERATOR = 'moderator'; 31 | 32 | /** 33 | * @var string 34 | */ 35 | const PUBLISHER_ONLY = 'publisheronly'; 36 | } 37 | -------------------------------------------------------------------------------- /src/OpenTok/Session.php: -------------------------------------------------------------------------------- 1 | 10 | * Use the \OpenTok\OpenTok->createSession() method to create an OpenTok session. Use the 11 | * getSessionId() method of the Session object to get the session ID. 12 | */ 13 | class Session 14 | { 15 | /** 16 | * @internal 17 | */ 18 | protected $sessionId; 19 | /** 20 | * @internal 21 | */ 22 | protected $location; 23 | /** 24 | * @internal 25 | */ 26 | protected $mediaMode; 27 | /** 28 | * @internal 29 | */ 30 | protected $archiveMode; 31 | /** 32 | * @internal 33 | */ 34 | protected $opentok; 35 | /** 36 | * @internal 37 | */ 38 | protected $e2ee; 39 | 40 | /** 41 | * @internal 42 | */ 43 | public function __construct($opentok, $sessionId, $properties = array()) 44 | { 45 | $defaults = [ 46 | 'mediaMode' => MediaMode::ROUTED, 47 | 'archiveMode' => ArchiveMode::MANUAL, 48 | 'location' => null, 49 | 'e2ee' => false 50 | ]; 51 | 52 | $properties = array_merge($defaults, array_intersect_key($properties, $defaults)); 53 | list($mediaMode, $archiveMode, $location, $e2ee) = array_values($properties); 54 | 55 | Validators::validateOpenTok($opentok); 56 | Validators::validateSessionId($sessionId); 57 | Validators::validateLocation($location); 58 | Validators::validateMediaMode($mediaMode); 59 | Validators::validateArchiveMode($archiveMode); 60 | 61 | $this->opentok = $opentok; 62 | $this->sessionId = $sessionId; 63 | $this->location = $location; 64 | $this->mediaMode = $mediaMode; 65 | $this->archiveMode = $archiveMode; 66 | $this->e2ee = $e2ee; 67 | } 68 | 69 | /** 70 | * Returns the session ID, which uniquely identifies the session. 71 | * 72 | * @return string 73 | */ 74 | public function getSessionId() 75 | { 76 | return $this->sessionId; 77 | } 78 | 79 | /** 80 | * Returns the location hint IP address. 81 | * 82 | * See OpenTok->createSession(). 83 | * 84 | * @return string 85 | */ 86 | public function getLocation() 87 | { 88 | return $this->location; 89 | } 90 | 91 | /** 92 | * Returns MediaMode::RELAYED if the session's streams will be transmitted directly between 93 | * peers; returns MediaMode::ROUTED if the session's streams will be transmitted using the 94 | * OpenTok Media Router. 95 | * 96 | * See OpenTok->createSession() 97 | * and MediaMode. 98 | * 99 | * @return MediaMode 100 | */ 101 | public function getMediaMode() 102 | { 103 | return $this->mediaMode; 104 | } 105 | 106 | /** 107 | * Defines whether the session is automatically archived (ArchiveMode::ALWAYS) 108 | * or not (ArchiveMode::MANUAL). 109 | * 110 | * See OpenTok->createSession() 111 | * and ArchiveMode. 112 | * 113 | * @return ArchiveMode 114 | */ 115 | public function getArchiveMode() 116 | { 117 | return $this->archiveMode; 118 | } 119 | 120 | /** 121 | * @internal 122 | */ 123 | public function __toString() 124 | { 125 | return $this->sessionId; 126 | } 127 | 128 | /** 129 | * Creates a token for connecting to the session. In order to authenticate a user, 130 | * the client passes a token when connecting to the session. 131 | *

132 | * For testing, you can also generate tokens or by logging in to your 133 | * OpenTok Video API account. 134 | * 135 | * @param array $options This array defines options for the token. This array include the 136 | * following keys, all of which are optional: 137 | * 138 | *

    139 | * 140 | *
  • 'role' (string) — One of the constants defined in the RoleConstants 141 | * class. The default role is publisher
  • 142 | * 143 | *
  • 'expireTime' (int) — The timestamp for when the token expires, 144 | * in milliseconds since the Unix epoch. The default expiration time is 24 hours 145 | * after the token creation time. The maximum expiration time is 30 days after the 146 | * token creation time.
  • 147 | * 148 | *
  • 'data' (string) — A string containing connection metadata 149 | * describing the end-user. For example, you can pass the user ID, name, or other data 150 | * describing the end-user. The length of the string is limited to 1000 characters. 151 | * This data cannot be updated once it is set.
  • 152 | * 153 | *
154 | * 155 | * @param bool $legacy Connection tokens are now SHA-256 signed JWTs. 156 | * Set this to true to create a token using the legacy T1 format. 157 | * 158 | * @return string The token string. 159 | */ 160 | public function generateToken($options = array(), bool $legacy = false) 161 | { 162 | return $this->opentok->generateToken($this->sessionId, $options, $legacy); 163 | } 164 | 165 | /** 166 | * Whether end-to-end encryption 167 | * is set for the session. 168 | * 169 | * @return bool 170 | */ 171 | public function getE2EE(): bool 172 | { 173 | return (bool)$this->e2ee; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/OpenTok/SipCall.php: -------------------------------------------------------------------------------- 1 | data['id'] = $sipCallData['id']; 30 | $this->data['connectionId'] = $sipCallData['connectionId']; 31 | $this->data['streamId'] = $sipCallData['streamId']; 32 | } 33 | 34 | /** 35 | * Returns the conference ID. 36 | */ 37 | /** @internal */ 38 | public function __get($name) 39 | { 40 | switch ($name) { 41 | case 'id': 42 | case 'connectionId': 43 | case 'streamId': 44 | return $this->data[$name]; 45 | default: 46 | return null; 47 | } 48 | } 49 | 50 | 51 | /** 52 | * Returns a JSON representation of this SipCall object. 53 | */ 54 | public function toJson() 55 | { 56 | return json_encode($this->data); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/OpenTok/Stream.php: -------------------------------------------------------------------------------- 1 | 8 | * See OpenTok.getStream() and 9 | * OpenTok.listStreams(). 10 | * 11 | * @property String $id 12 | * The stream ID. 13 | * 14 | * @property Array $layoutClassList 15 | * An array of the layout classes for the stream. 16 | * 17 | * @property String $name 18 | * The stream name (if one was set when the client published the stream). 19 | * 20 | * @property String $videoType 21 | * The type of video in the stream, which is set to either "camera" or "screen". 22 | */ 23 | 24 | class Stream 25 | { 26 | 27 | private $data; 28 | 29 | public function __construct($streamData) 30 | { 31 | 32 | $this->data = $streamData; 33 | } 34 | 35 | /** @ignore */ 36 | public function __get($name) 37 | { 38 | switch ($name) { 39 | case 'id': 40 | case 'videoType': 41 | case 'name': 42 | case 'layoutClassList': 43 | return $this->data[$name]; 44 | default: 45 | return null; 46 | } 47 | } 48 | 49 | public function jsonSerialize() 50 | { 51 | return $this->data; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/OpenTok/StreamList.php: -------------------------------------------------------------------------------- 1 | OpenTok.listStreams() 7 | * method, representing a list of streams in an OpenTok session. 8 | */ 9 | class StreamList 10 | { 11 | /** @ignore */ 12 | private $data; 13 | 14 | /** @ignore */ 15 | private $items; 16 | 17 | /** @ignore */ 18 | public function __construct($streamListData) 19 | { 20 | $this->data = $streamListData; 21 | } 22 | 23 | /** 24 | * Returns the number of total streams for the session ID. 25 | * 26 | * @return int 27 | */ 28 | public function totalCount() 29 | { 30 | return $this->data['count']; 31 | } 32 | 33 | /** 34 | * Returns an array of Stream objects. 35 | * 36 | * @return Stream[] 37 | */ 38 | public function getItems() 39 | { 40 | if (!is_array($this->items)) { 41 | $items = array(); 42 | foreach ($this->data['items'] as $streamData) { 43 | $items[] = new Stream($streamData); 44 | } 45 | $this->items = $items; 46 | } 47 | return $this->items; 48 | } 49 | 50 | /** 51 | * @return array 52 | */ 53 | public function jsonSerialize() 54 | { 55 | return $this->data; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/OpenTok/StreamMode.php: -------------------------------------------------------------------------------- 1 | getConstants(); 22 | } 23 | 24 | return self::$constCacheArray[$calledClass]; 25 | } 26 | 27 | public static function isValidName($name, $strict = false) 28 | { 29 | $constants = self::getConstants(); 30 | 31 | if ($strict) { 32 | return array_key_exists($name, $constants); 33 | } 34 | 35 | $keys = array_map('strtolower', array_keys($constants)); 36 | return in_array(strtolower($name), $keys); 37 | } 38 | 39 | public static function isValidValue($value) 40 | { 41 | $values = array_values(self::getConstants()); 42 | return in_array($value, $values, $strict = true); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/OpenTok/Util/archive-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "Archive List", 4 | "description": "A list of OpenTok Archives", 5 | "type": "object", 6 | "properties": { 7 | "count": { 8 | "type": "integer" 9 | }, 10 | "items": { 11 | "type": "array", 12 | "items": { 13 | "$ref": "#/definitions/archive" 14 | } 15 | } 16 | }, 17 | "required": ["count", "items"], 18 | "definitions": { 19 | "archive": { 20 | "title": "Archive", 21 | "description": "An OpenTok Archive", 22 | "type": "object", 23 | "properties": { 24 | "createdAt": { 25 | "type" : "integer" 26 | }, 27 | "duration": { 28 | "type" : "integer", 29 | "minimum": 0 30 | }, 31 | "id": { 32 | "type": "string", 33 | "pattern": "^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$" 34 | }, 35 | "name": { 36 | "type": ["string", "null"] 37 | }, 38 | "partnerId": { 39 | "type": "integer" 40 | }, 41 | "reason": { 42 | "type": "string" 43 | }, 44 | "sessionId": { 45 | "type": "string" 46 | }, 47 | "size": { 48 | "type": "integer", 49 | "minimum" : 0 50 | }, 51 | "status": { 52 | "type": "string", 53 | "enum": ["available", "started", "stopped", "failed", "deleted", "uploaded", "expired", "paused"] 54 | }, 55 | "url": { 56 | "type": ["string", "null"] 57 | }, 58 | "hasVideo": { 59 | "type": "boolean" 60 | }, 61 | "hasAudio": { 62 | "type": "boolean" 63 | } 64 | }, 65 | "required": [ "id", "partnerId", "sessionId", "status" ] 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/OpenTok/Util/broadcast-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "Broadcast", 4 | "description": "An OpenTok Broadcast", 5 | "type": "object", 6 | "properties": { 7 | "id": { 8 | "type": "string", 9 | "pattern": "^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$" 10 | }, 11 | "sessionId": { 12 | "type": "string" 13 | }, 14 | "partnerId": { 15 | "type": "integer" 16 | }, 17 | "createdAt": { 18 | "type" : "integer" 19 | }, 20 | "updatedAt": { 21 | "type" : "integer" 22 | }, 23 | "broadcastUrls": { 24 | "oneOf": [ 25 | { 26 | "type": "null" 27 | }, 28 | { 29 | "type": "object", 30 | "properties": { 31 | "hls": { 32 | "type": "string" 33 | } 34 | } 35 | } 36 | ] 37 | } 38 | }, 39 | "required": ["id", "sessionId", "partnerId", "createdAt", "updatedAt"] 40 | } 41 | -------------------------------------------------------------------------------- /tests/OpenTokTest/BroadcastTest.php: -------------------------------------------------------------------------------- 1 | broadcastData = [ 32 | 'id' => '063e72a4-64b4-43c8-9da5-eca071daab89', 33 | 'createdAt' => 1394394801000, 34 | 'updatedAt' => 1394394801000, 35 | 'partnerId' => 685, 36 | 'sessionId' => '2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-', 37 | 'multiBroadcastTag' => 'broadcast-1234b', 38 | 'layout' => [ 39 | 'type' => 'custom', 40 | 'stylesheet' => 'a layout stylesheet', 41 | 'screenshareType' => 'some options' 42 | ], 43 | 'maxDuration' => 5400, 44 | 'resolution' => '640x480', 45 | 'streamMode' => StreamMode::AUTO, 46 | 'status' => 'started', 47 | 'hasAudio' => true, 48 | 'hasVideo' => true 49 | ]; 50 | } 51 | 52 | public static function setUpBeforeClass(): void 53 | { 54 | self::$mockBasePath = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'mock' . DIRECTORY_SEPARATOR; 55 | } 56 | 57 | public function setupBroadcasts($streamMode) 58 | { 59 | $data = $this->broadcastData; 60 | $data['streamMode'] = $streamMode; 61 | 62 | $this->broadcast = new Broadcast($data, array( 63 | 'apiKey' => $this->API_KEY, 64 | 'apiSecret' => $this->API_SECRET, 65 | 'client' => $this->client 66 | )); 67 | } 68 | 69 | private function setupOTWithMocks($mocks) 70 | { 71 | $this->API_KEY = defined('API_KEY') ? API_KEY : '12345678'; 72 | $this->API_SECRET = defined('API_SECRET') ? API_SECRET : 'b60d0b2568f3ea9731bd9d3f71be263ce19f802f'; 73 | 74 | if (is_array($mocks)) { 75 | $responses = TestHelpers::mocksToResponses($mocks, self::$mockBasePath); 76 | } else { 77 | $responses = []; 78 | } 79 | 80 | $mock = new MockHandler($responses); 81 | $handlerStack = HandlerStack::create($mock); 82 | $clientOptions = [ 83 | 'handler' => $handlerStack 84 | ]; 85 | 86 | $this->client = new Client(); 87 | $this->client->configure( 88 | $this->API_KEY, 89 | $this->API_SECRET, 90 | 'https://api.opentok.com', 91 | $clientOptions 92 | ); 93 | 94 | // Push history onto handler stack *after* configuring client to 95 | // ensure auth header is added before history handler is invoked 96 | $this->historyContainer = []; 97 | $history = Middleware::history($this->historyContainer); 98 | $handlerStack->push($history); 99 | } 100 | 101 | private function setupOT() 102 | { 103 | return $this->setupOTWithMocks([]); 104 | } 105 | 106 | public function testInitializes(): void 107 | { 108 | $this->setupOT(); 109 | $this->setupBroadcasts(StreamMode::AUTO); 110 | $this->assertInstanceOf(Broadcast::class, $this->broadcast); 111 | 112 | } 113 | 114 | public function testCannotAddStreamToBroadcastInAutoMode(): void 115 | { 116 | $this->expectException(InvalidArgumentException::class); 117 | $this->setupOTWithMocks([[ 118 | 'code' => 200, 119 | 'headers' => [ 120 | 'Content-Type' => 'application/json' 121 | ], 122 | 'path' => 'v2/project/APIKEY/broadcast/BROADCASTID/get' 123 | ]]); 124 | 125 | $this->setupBroadcasts(StreamMode::AUTO); 126 | 127 | $this->broadcast->addStreamToBroadcast( 128 | '5dfds4-asdda4asf4', 129 | true, 130 | true 131 | ); 132 | } 133 | 134 | public function testCannotAddStreamToBroadcastWithNoAudioAndVideo(): void 135 | { 136 | $this->expectException(InvalidArgumentException::class); 137 | $this->setupOTWithMocks([[ 138 | 'code' => 200, 139 | 'headers' => [ 140 | 'Content-Type' => 'application/json' 141 | ], 142 | 'path' => 'v2/project/APIKEY/broadcast/BROADCASTID/get' 143 | ]]); 144 | 145 | $this->setupBroadcasts(StreamMode::MANUAL); 146 | 147 | $this->broadcast->addStreamToBroadcast( 148 | '5dfds4-asdda4asf4', 149 | false, 150 | false 151 | ); 152 | } 153 | 154 | public function testCanAddStreamToBroadcast(): void 155 | { 156 | $this->setupOTWithMocks([[ 157 | 'code' => 200, 158 | 'headers' => [ 159 | 'Content-Type' => 'application/json' 160 | ], 161 | 'path' => 'v2/project/APIKEY/broadcast/BROADCASTID/get' 162 | ]]); 163 | 164 | $this->setupBroadcasts(StreamMode::MANUAL); 165 | 166 | $return = $this->broadcast->addStreamToBroadcast( 167 | '5dfds4-asdda4asf4', 168 | true, 169 | true 170 | ); 171 | $this->assertTrue($return); 172 | } 173 | 174 | public function testCanRemoveStreamFromBroadcast(): void 175 | { 176 | $this->setupOTWithMocks([[ 177 | 'code' => 200, 178 | 'headers' => [ 179 | 'Content-Type' => 'application/json' 180 | ], 181 | 'path' => 'v2/project/APIKEY/broadcast/BROADCASTID/get' 182 | ]]); 183 | 184 | $this->setupBroadcasts(StreamMode::MANUAL); 185 | 186 | $return = $this->broadcast->removeStreamFromBroadcast( 187 | '5dfds4-asdda4asf4' 188 | ); 189 | $this->assertTrue($return); 190 | } 191 | 192 | public function testCannotRemoveStreamFromBroadcastOnAuto(): void 193 | { 194 | $this->expectException(InvalidArgumentException::class); 195 | 196 | $this->setupOTWithMocks([[ 197 | 'code' => 200, 198 | 'headers' => [ 199 | 'Content-Type' => 'application/json' 200 | ], 201 | 'path' => 'v2/project/APIKEY/broadcast/BROADCASTID/get' 202 | ]]); 203 | 204 | $this->setupBroadcasts(StreamMode::AUTO); 205 | 206 | $return = $this->broadcast->removeStreamFromBroadcast( 207 | '5dfds4-asdda4asf4' 208 | ); 209 | } 210 | 211 | public function testGetters(): void 212 | { 213 | $broadcastObject = new Broadcast($this->broadcastData, [ 214 | 'apiKey' => 'abc', 215 | 'apiSecret' => 'efg', 216 | 'client' => $this->client 217 | ]); 218 | 219 | $this->assertTrue($broadcastObject->hasAudio); 220 | $this->assertTrue($broadcastObject->hasVideo); 221 | $this->assertEquals('broadcast-1234b', $broadcastObject->multiBroadcastTag); 222 | $this->assertEquals('started', $broadcastObject->status); 223 | $this->assertNull($broadcastObject->wrongKey); 224 | } 225 | } 226 | 227 | -------------------------------------------------------------------------------- /tests/OpenTokTest/LayoutTest.php: -------------------------------------------------------------------------------- 1 | Layout::getBestFit(), 15 | Layout::LAYOUT_HORIZONTAL => Layout::getHorizontalPresentation(), 16 | Layout::LAYOUT_PIP => Layout::getPIP(), 17 | Layout::LAYOUT_VERTICAL => Layout::getVerticalPresentation(), 18 | ]; 19 | 20 | foreach ($layouts as $type => $object) { 21 | $this->assertSame(['type' => $type], $object->toArray()); 22 | } 23 | } 24 | 25 | public function testWillValidateLayout(): void 26 | { 27 | $this->expectException(\InvalidArgumentException::class); 28 | $object = ['bestFit' => true]; 29 | 30 | Validators::validateLayout($object); 31 | } 32 | 33 | public function testStylesheetIsInSerializedArrayIfCustom() 34 | { 35 | $layout = Layout::createCustom(['stylesheet' => 'foo']); 36 | 37 | $this->assertSame(['type' => LAYOUT::LAYOUT_CUSTOM, 'stylesheet' => 'foo'], $layout->toArray()); 38 | } 39 | 40 | public function testScreenshareTypeSerializesProperly() 41 | { 42 | $layout = Layout::getBestFit(); 43 | $layout->setScreenshareType(Layout::LAYOUT_PIP); 44 | 45 | $expected = [ 46 | 'type' => 'bestFit', 47 | 'screenshareType' => 'pip' 48 | ]; 49 | 50 | $this->assertSame($expected, $layout->toArray()); 51 | } 52 | 53 | public function testScreenshareTypeCannotBeSetToInvalidValue() 54 | { 55 | $this->expectException(\RuntimeException::class); 56 | $this->expectExceptionMessage('Screenshare type must be of a valid layout type'); 57 | 58 | $layout = Layout::getBestFit(); 59 | $layout->setScreenshareType('bar'); 60 | } 61 | 62 | public function testScreenshareTypeCannotBeSetOnInvalidLayoutType() 63 | { 64 | $this->expectException(\RuntimeException::class); 65 | $this->expectExceptionMessage('Screenshare type cannot be set on a layout type other than bestFit'); 66 | 67 | $layout = Layout::createCustom(['stylesheet' => 'foo']); 68 | $layout->setScreenshareType(Layout::LAYOUT_PIP); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/OpenTokTest/RenderTest.php: -------------------------------------------------------------------------------- 1 | expectError(\TypeError::class); 13 | $payload = [ 14 | 'id' => '1248e7070b81464c9789f46ad10e7764', 15 | 'sessionId' => '2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4', 16 | 'projectId' => 'e2343f23456g34709d2443a234', 17 | 'createdAt' => 1437676551000, 18 | 'updatedAt' => 1437676551000, 19 | 'url' => 'https://webapp.customer.com', 20 | 'resolution' => '1280x720', 21 | 'status«' => 'started', 22 | 'streamId' => 'e32445b743678c98230f238' 23 | ]; 24 | 25 | $render = new Render($payload); 26 | } 27 | 28 | public function testCanHydrateFromPayload(): void 29 | { 30 | $payload = [ 31 | 'id' => '1248e7070b81464c9789f46ad10e7764', 32 | 'sessionId' => '2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4', 33 | 'projectId' => 'e2343f23456g34709d2443a234', 34 | 'createdAt' => 1437676551000, 35 | 'updatedAt' => 1437676551000, 36 | 'url' => 'https://webapp.customer.com', 37 | 'resolution' => '1280x720', 38 | 'status' => 'started', 39 | 'streamId' => 'e32445b743678c98230f238' 40 | ]; 41 | 42 | $jsonPayload = json_encode($payload); 43 | 44 | $render = new Render($jsonPayload); 45 | 46 | $this->assertEquals('1248e7070b81464c9789f46ad10e7764', $render->id); 47 | $this->assertEquals('2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4', $render->sessionId); 48 | $this->assertEquals('e2343f23456g34709d2443a234', $render->projectId); 49 | $this->assertEquals(1437676551000, $render->createdAt); 50 | $this->assertEquals(1437676551000, $render->updatedAt); 51 | $this->assertEquals('https://webapp.customer.com', $render->url); 52 | $this->assertEquals('1280x720', $render->resolution); 53 | $this->assertEquals('started', $render->status); 54 | $this->assertEquals('e32445b743678c98230f238', $render->streamId); 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /tests/OpenTokTest/SessionTest.php: -------------------------------------------------------------------------------- 1 | API_KEY = defined('API_KEY') ? API_KEY : '12345678'; 27 | $this->API_SECRET = defined('API_SECRET') ? API_SECRET : 'b60d0b2568f3ea9731bd9d3f71be263ce19f802f'; 28 | $this->opentok = new OpenTok($this->API_KEY, $this->API_SECRET); 29 | } 30 | 31 | public function testSessionWithId() 32 | { 33 | $sessionId = 'SESSIONID'; 34 | $session = new Session($this->opentok, $sessionId); 35 | $this->assertEquals($sessionId, $session->getSessionId()); 36 | $this->assertEquals(MediaMode::ROUTED, $session->getMediaMode()); 37 | $this->assertEmpty($session->getLocation()); 38 | } 39 | 40 | public function testSessionWithIdAndLocation() 41 | { 42 | $sessionId = 'SESSIONID'; 43 | $location = '12.34.56.78'; 44 | $session = new Session($this->opentok, $sessionId, array( 'location' => $location )); 45 | $this->assertEquals($sessionId, $session->getSessionId()); 46 | $this->assertEquals(MediaMode::ROUTED, $session->getMediaMode()); 47 | $this->assertEquals($location, $session->getLocation()); 48 | } 49 | 50 | public function testSessionWithIdAndMediaMode() 51 | { 52 | $sessionId = 'SESSIONID'; 53 | $mediaMode = MediaMode::RELAYED; 54 | $session = new Session($this->opentok, $sessionId, array( 'mediaMode' => $mediaMode )); 55 | $this->assertEquals($sessionId, $session->getSessionId()); 56 | $this->assertEquals($mediaMode, $session->getMediaMode()); 57 | $this->assertEmpty($session->getLocation()); 58 | 59 | $mediaMode = MediaMode::ROUTED; 60 | $session = new Session($this->opentok, $sessionId, array( 'mediaMode' => $mediaMode )); 61 | $this->assertEquals($sessionId, $session->getSessionId()); 62 | $this->assertEquals($mediaMode, $session->getMediaMode()); 63 | $this->assertEmpty($session->getLocation()); 64 | } 65 | 66 | public function testSessionWithIdAndLocationAndMediaMode() 67 | { 68 | $sessionId = 'SESSIONID'; 69 | $location = '12.34.56.78'; 70 | $mediaMode = MediaMode::RELAYED; 71 | $session = new Session($this->opentok, $sessionId, array( 'location' => $location, 'mediaMode' => $mediaMode )); 72 | $this->assertEquals($sessionId, $session->getSessionId()); 73 | $this->assertEquals($mediaMode, $session->getMediaMode()); 74 | $this->assertEquals($location, $session->getLocation()); 75 | 76 | $mediaMode = MediaMode::ROUTED; 77 | $session = new Session($this->opentok, $sessionId, array( 'location' => $location, 'mediaMode' => $mediaMode )); 78 | $this->assertEquals($sessionId, $session->getSessionId()); 79 | $this->assertEquals($mediaMode, $session->getMediaMode()); 80 | $this->assertEquals($location, $session->getLocation()); 81 | } 82 | 83 | public function testSessionWithArchiveMode() 84 | { 85 | $sessionId = 'SESSIONID'; 86 | $archiveMode = ArchiveMode::ALWAYS; 87 | $session = new Session($this->opentok, $sessionId, array( 'archiveMode' => $archiveMode )); 88 | $this->assertEquals($sessionId, $session->getSessionId()); 89 | $this->assertEquals($archiveMode, $session->getArchiveMode()); 90 | 91 | $archiveMode = ArchiveMode::MANUAL; 92 | $session = new Session($this->opentok, $sessionId, array( 'archiveMode' => $archiveMode )); 93 | $this->assertEquals($sessionId, $session->getSessionId()); 94 | $this->assertEquals($archiveMode, $session->getArchiveMode()); 95 | } 96 | 97 | /** 98 | * @dataProvider badParameterProvider 99 | */ 100 | public function testInitializationWithBadParams($sessionId, $props) 101 | { 102 | $this->expectException('InvalidArgumentException'); 103 | if (!$props || empty($props)) { 104 | $session = new Session($this->opentok, $sessionId); 105 | } else { 106 | $session = new Session($this->opentok, $sessionId, $props); 107 | } 108 | } 109 | 110 | public function badParameterProvider() 111 | { 112 | return array( 113 | array(array(), array()), 114 | array('SESSIONID', array( 'location' => 'NOTALOCATION') ), 115 | array('SESSIONID', array( 'mediaMode' => 'NOTAMODE' ) ), 116 | array('SESSIONID', array( 'location' => '127.0.0.1', 'mediaMode' => 'NOTAMODE' ) ), 117 | array('SESSIONID', array( 'location' => 'NOTALOCATION', 'mediaMode' => MediaMode::RELAYED ) ) 118 | ); 119 | } 120 | 121 | public function testInitialzationWithoutE2ee() 122 | { 123 | $sessionId = 'SESSIONID'; 124 | $session = new Session($this->opentok, $sessionId); 125 | $this->assertEquals(false, $session->getE2EE()); 126 | } 127 | 128 | public function testInitialzationWithE2ee() 129 | { 130 | $sessionId = 'SESSIONID'; 131 | $session = new Session($this->opentok, $sessionId, ['e2ee' => true]); 132 | $this->assertEquals(true, $session->getE2EE()); 133 | } 134 | 135 | public function testInitializationWithExtraneousParams() 136 | { 137 | $sessionId = 'SESSIONID'; 138 | $session = new Session($this->opentok, $sessionId, array( 'notrealproperty' => 'notrealvalue' )); 139 | $this->assertEquals($sessionId, $session->getSessionId()); 140 | $this->assertEmpty($session->getLocation()); 141 | $this->assertEquals(MediaMode::ROUTED, $session->getMediaMode()); 142 | } 143 | 144 | public function testCastingToString() 145 | { 146 | $sessionId = 'SESSIONID'; 147 | $session = new Session($this->opentok, $sessionId); 148 | $this->assertEquals($sessionId, (string)$session); 149 | } 150 | 151 | public function testGeneratesToken() 152 | { 153 | $sessionId = '1_MX4xMjM0NTY3OH4-VGh1IEZlYiAyNyAwNDozODozMSBQU1QgMjAxNH4wLjI0NDgyMjI'; 154 | $bogusApiKey = '12345678'; 155 | $bogusApiSecret = 'b60d0b2568f3ea9731bd9d3f71be263ce19f802f'; 156 | $opentok = new OpenTok($bogusApiKey, $bogusApiSecret); 157 | $session = new Session($opentok, $sessionId); 158 | 159 | $token = $session->generateToken([], true); 160 | 161 | $this->assertIsString($token); 162 | $decodedToken = TestHelpers::decodeToken($token); 163 | $this->assertEquals($sessionId, $decodedToken['session_id']); 164 | $this->assertEquals($bogusApiKey, $decodedToken['partner_id']); 165 | $this->assertNotEmpty($decodedToken['nonce']); 166 | $this->assertNotEmpty($decodedToken['create_time']); 167 | $this->assertArrayNotHasKey('connection_data', $decodedToken); 168 | // TODO: should all tokens have a role of publisher even if this wasn't specified? 169 | //$this->assertNotEmpty($decodedToken['role']); 170 | // TODO: should all tokens have a default expire time even if it wasn't specified? 171 | //$this->assertNotEmpty($decodedToken['expire_time']); 172 | 173 | $this->assertNotEmpty($decodedToken['sig']); 174 | $this->assertEquals(hash_hmac('sha1', $decodedToken['dataString'], $bogusApiSecret), $decodedToken['sig']); 175 | } 176 | } -------------------------------------------------------------------------------- /tests/OpenTokTest/SipCallTest.php: -------------------------------------------------------------------------------- 1 | '1_MX4xMjM0NTY3OH4', 13 | 'connectionId' => 'VGh1IEZlYiAyNyAwNDozODozMSBQU1QgMjAxNH4wLjI0NDgyMjI', 14 | 'streamId' => 'b60d0b2568f3ea9731bd9d3f71be263ce19f802f' 15 | ]; 16 | 17 | $sipCall = new SipCall($sipCallData); 18 | 19 | $this->assertEquals('1_MX4xMjM0NTY3OH4', $sipCall->id); 20 | $this->assertEquals('VGh1IEZlYiAyNyAwNDozODozMSBQU1QgMjAxNH4wLjI0NDgyMjI', $sipCall->connectionId); 21 | $this->assertEquals('b60d0b2568f3ea9731bd9d3f71be263ce19f802f', $sipCall->streamId); 22 | $this->assertNull($sipCall->observeForceMute); 23 | } 24 | } -------------------------------------------------------------------------------- /tests/OpenTokTest/TestHelpers.php: -------------------------------------------------------------------------------- 1 | $parts[1] 34 | )); 35 | } 36 | 37 | public static function validateOpenTokAuthHeader($apiKey, $apiSecret, $token) 38 | { 39 | if (!isset($token)) { 40 | return false; 41 | } 42 | 43 | try { 44 | $decodedToken = JWT::decode($token, new Key($apiSecret, 'HS256')); 45 | } catch (\Exception $e) { 46 | return false; 47 | } 48 | 49 | if (!property_exists($decodedToken, 'iss') || $decodedToken->iss !== $apiKey) { 50 | return false; 51 | } 52 | 53 | if (!property_exists($decodedToken, 'ist') || 'project' !== $decodedToken->ist) { 54 | return false; 55 | } 56 | 57 | if (!property_exists($decodedToken, 'exp') || time() >= $decodedToken->exp) { 58 | return false; 59 | } 60 | 61 | if (!property_exists($decodedToken, 'jti')) { 62 | return false; 63 | } 64 | 65 | return true; 66 | } 67 | 68 | public static function mocksToResponses($mocks, $basePath) 69 | { 70 | return array_map(function ($mock) use ($basePath) { 71 | $code = !empty($mock['code']) ? $mock['code'] : 200; 72 | $headers = !empty($mock['headers']) ? $mock['headers'] : []; 73 | $body = null; 74 | if (!empty($mock['body'])) { 75 | $body = $mock['body']; 76 | } elseif (!empty($mock['path'])) { 77 | $body = file_get_contents($basePath . $mock['path']); 78 | } 79 | return new Response($code, $headers, $body); 80 | }, $mocks); 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /tests/OpenTokTest/Util/ClientTest.php: -------------------------------------------------------------------------------- 1 | getResponse('connect') 21 | ]); 22 | $handlerStack = HandlerStack::create($mock); 23 | $guzzle = new GuzzleHttpClient(['handler' => $handlerStack]); 24 | 25 | $client = new Client(); 26 | $client->configure('asdf', 'asdf', 'http://localhost/', ['client' => $guzzle]); 27 | 28 | $websocketDummy = [ 29 | 'uri' => 'ws://test' 30 | ]; 31 | 32 | $response = $client->connectAudio('ddd', 'sarar55r', $websocketDummy); 33 | $this->assertEquals('063e72a4-64b4-43c8-9da5-eca071daab89', $response['id']); 34 | $this->assertEquals('7aebb3a4-3d86-4962-b317-afb73e05439d', $response['connectionId']); 35 | } 36 | 37 | public function testHandlesSignalErrorHandles400Response() 38 | { 39 | $this->expectException(SignalUnexpectedValueException::class); 40 | 41 | $mock = new MockHandler([ 42 | $this->getResponse('signal-failure-payload', 400) 43 | ]); 44 | $handlerStack = HandlerStack::create($mock); 45 | $guzzle = new GuzzleHttpClient(['handler' => $handlerStack]); 46 | 47 | $client = new Client(); 48 | $client->configure('asdf', 'asdf', 'http://localhost/', ['client' => $guzzle]); 49 | $client->signal('sessionabcd', ['type' => 'foo', 'data' => 'bar'], 'connection1234'); 50 | } 51 | 52 | public function testHandlesSignalErrorHandles403Response() 53 | { 54 | $this->expectException(SignalAuthenticationException::class); 55 | 56 | $mock = new MockHandler([ 57 | $this->getResponse('signal-failure-invalid-token', 403) 58 | ]); 59 | $handlerStack = HandlerStack::create($mock); 60 | $guzzle = new GuzzleHttpClient(['handler' => $handlerStack]); 61 | 62 | $client = new Client(); 63 | $client->configure('asdf', 'asdf', 'http://localhost/', ['client' => $guzzle]); 64 | $client->signal('sessionabcd', ['type' => 'foo', 'data' => 'bar'], 'connection1234'); 65 | } 66 | 67 | public function testHandlesSignalErrorHandles404Response() 68 | { 69 | $this->expectException(SignalConnectionException::class); 70 | $this->expectExceptionMessage('The client specified by the connectionId property is not connected to the session.'); 71 | 72 | $mock = new MockHandler([ 73 | $this->getResponse('signal-failure-no-clients', 404) 74 | ]); 75 | $handlerStack = HandlerStack::create($mock); 76 | $guzzle = new GuzzleHttpClient(['handler' => $handlerStack]); 77 | 78 | $client = new Client(); 79 | $client->configure('asdf', 'asdf', 'http://localhost/', ['client' => $guzzle]); 80 | $client->signal('sessionabcd', ['type' => 'foo', 'data' => 'bar'], 'connection1234'); 81 | } 82 | 83 | public function testHandlesSignalErrorHandles413Response() 84 | { 85 | $this->expectException(SignalUnexpectedValueException::class); 86 | $this->expectExceptionMessage('The type string exceeds the maximum length (128 bytes), or the data string exceeds the maximum size (8 kB).'); 87 | 88 | $mock = new MockHandler([ 89 | $this->getResponse('signal-failure', 413) 90 | ]); 91 | $handlerStack = HandlerStack::create($mock); 92 | $guzzle = new GuzzleHttpClient(['handler' => $handlerStack]); 93 | 94 | $client = new Client(); 95 | $client->configure('asdf', 'asdf', 'http://localhost/', ['client' => $guzzle]); 96 | $client->signal('sessionabcd', ['type' => 'foo', 'data' => 'bar'], 'connection1234'); 97 | } 98 | 99 | /** 100 | * Get the API response we'd expect for a call to the API. 101 | */ 102 | protected function getResponse(string $type = 'success', int $status = 200): Response 103 | { 104 | return new Response( 105 | $status, 106 | ['Content-Type' => 'application/json'], 107 | fopen(__DIR__ . '/responses/' . $type . '.json', 'rb') 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/OpenTokTest/Util/responses/connect.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "063e72a4-64b4-43c8-9da5-eca071daab89", 3 | "connectionId": "7aebb3a4-3d86-4962-b317-afb73e05439d" 4 | } -------------------------------------------------------------------------------- /tests/OpenTokTest/Util/responses/signal-failure-invalid-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": -1, 3 | "message": "Id in the token does not match the one in the url", 4 | "description": "Id in the token does not match the one in the url" 5 | } -------------------------------------------------------------------------------- /tests/OpenTokTest/Util/responses/signal-failure-no-clients.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "Not found. No clients are actively connected to the OpenTok session." 3 | } -------------------------------------------------------------------------------- /tests/OpenTokTest/Util/responses/signal-failure-payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 15202, 3 | "message": "Signal payload 'data' must be set", 4 | "description": "Signal payload 'data' must be set" 5 | } -------------------------------------------------------------------------------- /tests/OpenTokTest/Util/responses/signal-failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": -99, 3 | "message": "Unknown error", 4 | "description": "An unknown error occurred" 5 | } -------------------------------------------------------------------------------- /tests/OpenTokTest/Validators/ValidatorsTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(Validators::isVonageKeypair($apiKey, $apiSecret)); 17 | } 18 | 19 | public function testIsNotVonageKeypair(): void 20 | { 21 | $apiKey = '4349501'; 22 | $apiSecret = 'b60d0b2568f3ea9731bd9d3f71be263ce19f802f'; 23 | $this->assertFalse(Validators::isVonageKeypair($apiKey, $apiSecret)); 24 | } 25 | 26 | public function testVonageKeypairFailsWithOnlyAppId(): void 27 | { 28 | $this->expectException(InvalidArgumentException::class); 29 | $apiKey = '1ab38a10-ed9d-4e2b-8b14-95e52d76a13c'; 30 | $apiSecret = 'b60d0b2568f3ea9731bd9d3f71be263ce19f802f'; 31 | Validators::isVonageKeypair($apiKey, $apiSecret); 32 | } 33 | 34 | public function testVonageKeypairFailsWithOnlyPrivateKey(): void 35 | { 36 | $this->expectException(InvalidArgumentException::class); 37 | $apiKey = '1ab38a10-ed9d-4e2b-8b14-95e52d76a13c'; 38 | $apiSecret = 'b60d0b2568f3ea9731bd9d3f71be263ce19f802f'; 39 | Validators::isVonageKeypair($apiKey, $apiSecret); 40 | } 41 | 42 | public function testWillValidateApiUrl(): void 43 | { 44 | $this->expectNotToPerformAssertions(); 45 | $apiUrl = 'https://api.opentok.com'; 46 | Validators::validateApiUrl($apiUrl); 47 | } 48 | 49 | public function testWillInvalidateApiUrl(): void 50 | { 51 | $this->expectException(InvalidArgumentException::class); 52 | $apiUrl = 'dave@opentok.com'; 53 | Validators::validateApiUrl($apiUrl); 54 | } 55 | 56 | public function testWillPassCorrectForceMutePayload(): void 57 | { 58 | $this->expectNotToPerformAssertions(); 59 | 60 | $options = [ 61 | 'excludedStreams' => [ 62 | 'streamId1', 63 | 'streamId2' 64 | ], 65 | 'active' => true 66 | ]; 67 | 68 | Validators::validateForceMuteAllOptions($options); 69 | } 70 | 71 | public function testIsAssocWithIndexedArray(): void 72 | { 73 | $array = [1, 2, 3, 4]; 74 | $this->assertFalse(Validators::isAssoc($array)); 75 | } 76 | 77 | public function testIsAssocWithAssociativeArray(): void 78 | { 79 | $array = ['a' => 1, 'b' => 2, 'c' => 3]; 80 | $this->assertTrue(Validators::isAssoc($array)); 81 | } 82 | 83 | public function testIsAssocWithMixedKeysArray(): void 84 | { 85 | $array = [1, 'a' => 2, 3]; 86 | $this->assertTrue(Validators::isAssoc($array)); 87 | } 88 | 89 | public function testIsAssocWithEmptyArray(): void 90 | { 91 | $array = []; 92 | $this->assertFalse(Validators::isAssoc($array)); 93 | } 94 | 95 | public function testWillFailWhenStreamIdsAreNotCorrect(): void 96 | { 97 | $this->expectException(InvalidArgumentException::class); 98 | 99 | $options = [ 100 | 'excludedStreams' => [ 101 | 3536, 102 | 'streamId2' 103 | ], 104 | 'active' => true 105 | ]; 106 | 107 | Validators::validateForceMuteAllOptions($options); 108 | } 109 | 110 | public function testWillFailWhenActiveIsNotBool(): void 111 | { 112 | $this->expectException(InvalidArgumentException::class); 113 | 114 | $options = [ 115 | 'excludedStreams' => [ 116 | 'streamId1', 117 | 'streamId2' 118 | ], 119 | 'active' => 'true' 120 | ]; 121 | 122 | Validators::validateForceMuteAllOptions($options); 123 | } 124 | 125 | public function testWillFailWhenStreamIdsIsNotArray(): void 126 | { 127 | $this->expectException(InvalidArgumentException::class); 128 | 129 | $options = [ 130 | 'excludedStreams' => 'streamIdOne', 131 | 'active' => false 132 | ]; 133 | 134 | Validators::validateForceMuteAllOptions($options); 135 | } 136 | 137 | public function testWillValidateWebsocketConfiguration(): void 138 | { 139 | $this->expectNotToPerformAssertions(); 140 | $websocketConfig = [ 141 | 'uri' => 'ws://valid-websocket', 142 | 'streams' => [ 143 | '525503c7-913e-43a1-84b4-31b2e9fe668b', 144 | '14026813-4f50-4a5a-9b72-fea25430916d' 145 | ] 146 | ]; 147 | Validators::validateWebsocketOptions($websocketConfig); 148 | } 149 | 150 | public function testWillThrowExceptionOnInvalidWebsocketConfiguration(): void 151 | { 152 | $this->expectException(InvalidArgumentException::class); 153 | 154 | $websocketConfig = [ 155 | 'streams' => [ 156 | '525503c7-913e-43a1-84b4-31b2e9fe668b', 157 | '14026813-4f50-4a5a-9b72-fea25430916d' 158 | ] 159 | ]; 160 | Validators::validateWebsocketOptions($websocketConfig); 161 | } 162 | 163 | /** 164 | * @dataProvider resolutionProvider 165 | */ 166 | public function testValidResolutions($resolution, $isValid): void 167 | { 168 | if ( ! $isValid) { 169 | $this->expectException(InvalidArgumentException::class); 170 | } else { 171 | $this->expectNotToPerformAssertions(); 172 | } 173 | 174 | Validators::validateResolution($resolution); 175 | } 176 | 177 | public function testValidLayoutClassListItemErrorOnString(): void 178 | { 179 | $input = 'example_id'; 180 | $this->expectException(\InvalidArgumentException::class); 181 | Validators::validateLayoutClassListItem($input); 182 | } 183 | 184 | public function testValidLayoutClassListItem(): void 185 | { 186 | $layoutClassList = [ 187 | 'id' => 'example_id', 188 | 'layoutClassList' => ['class1', 'class2'] 189 | ]; 190 | 191 | $this->assertNull(Validators::validateLayoutClassListItem($layoutClassList)); 192 | } 193 | 194 | public function testInvalidIdType(): void 195 | { 196 | $this->expectException(InvalidArgumentException::class); 197 | $layoutClassList = [ 198 | 'id' => 123, 199 | 'layoutClassList' => ['class1', 'class2'] 200 | ]; 201 | 202 | Validators::validateLayoutClassListItem($layoutClassList); 203 | } 204 | 205 | public function testMissingLayoutClassList(): void 206 | { 207 | $this->expectException(InvalidArgumentException::class); 208 | $layoutClassList = [ 209 | 'id' => 'example_id' 210 | ]; 211 | 212 | Validators::validateLayoutClassListItem($layoutClassList); 213 | } 214 | 215 | public function testInvalidLayoutClassListType(): void 216 | { 217 | $this->expectException(InvalidArgumentException::class); 218 | $layoutClassList = [ 219 | 'id' => 'example_id', 220 | 'layoutClassList' => 'invalid_class' 221 | ]; 222 | 223 | Validators::validateLayoutClassListItem($layoutClassList); 224 | } 225 | public function testValidateClient(): void 226 | { 227 | $client = new Client(); 228 | Validators::validateClient($client); 229 | 230 | // No exception, which was the test so fake a pass 231 | $this->assertTrue(true); 232 | } 233 | 234 | public function testExceptionOnInvalidClient(): void 235 | { 236 | $this->expectException(\InvalidArgumentException::class); 237 | $client = new \stdClass(); 238 | Validators::validateClient($client); 239 | } 240 | 241 | public function testThrowsErrorOnInvalidStreamMode(): void 242 | { 243 | $this->expectException(\InvalidArgumentException::class); 244 | $streamMode = ['auto']; 245 | Validators::validateHasStreamMode($streamMode); 246 | } 247 | 248 | public function testValidateSignalPayload(): void 249 | { 250 | $validPayload = ['type' => 'signal_type', 'data' => 'signal_data']; 251 | $this->assertNull(Validators::validateSignalPayload($validPayload)); 252 | 253 | $invalidDataPayload = ['type' => 'signal_type', 'data' => 123]; 254 | $this->expectException(InvalidArgumentException::class); 255 | $this->expectExceptionMessage("Signal Payload cannot be null:"); 256 | Validators::validateSignalPayload($invalidDataPayload); 257 | 258 | $invalidTypePayload = ['type' => null, 'data' => 'signal_data']; 259 | $this->expectException(InvalidArgumentException::class); 260 | $this->expectExceptionMessage("Signal Payload cannot be null:"); 261 | Validators::validateSignalPayload($invalidTypePayload); 262 | 263 | // Invalid payload: both type and data are null 264 | $invalidBothPayload = ['type' => null, 'data' => null]; 265 | $this->expectException(InvalidArgumentException::class); 266 | $this->expectExceptionMessage("Signal Payload cannot be null:"); 267 | Validators::validateSignalPayload($invalidBothPayload); 268 | } 269 | 270 | /** 271 | * @dataProvider connectionIdProvider 272 | */ 273 | public function testConnectionId($input, $expectException): void 274 | { 275 | if ($expectException) { 276 | $this->expectException(\InvalidArgumentException::class); 277 | } 278 | 279 | Validators::validateConnectionId($input); 280 | $this->assertTrue(true); 281 | } 282 | 283 | public function connectionIdProvider(): array 284 | { 285 | return [ 286 | [['this' => 'is not a string'], true], 287 | ['', true], 288 | ['valid_connection_string', false] 289 | ]; 290 | } 291 | 292 | public function resolutionProvider(): array 293 | { 294 | return [ 295 | ['640x480', true], 296 | ['1280x720', true], 297 | ['1920x1080', true], 298 | ['480x640', true], 299 | ['720x1280', true], 300 | ['1080x1920', true], 301 | ['1080X1920', true], 302 | ['923x245', false] 303 | ]; 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /tests/OpenTokTest/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCy3YiiZ206+/7j 3 | oDzF9qGHhFuxEGuGL1ufGm0LvCiOgNJpV4KGatjdminomS0PwI6v9Gz3r4mYBxeR 4 | 3xXV3xPpr3yEDu+ivQN8oMei4ttg++nuyk26gjAdEvz5uSQBV5lWvgjR6tlSPb/j 5 | ca4d5AymuWmT110qcQ+ui7eoOWAfQIWj5Tlqk6YyXUnoMlD4c2c9/hOJZR51VF1n 6 | diDONkHplqjzGfdHxDxCBXsIW1wi8h6PVHH54rDmYay1ojRVPJ7b2RBu8vgvWYoR 7 | du6cY8Bp/1skQUEvtOBhFN62vAZ12s91jFO+plzyA9oDfAXVguW0yRWj5GNJmtPZ 8 | YIcynpjvAgMBAAECggEAM1tzq3n5/Zk0jyRHvum5aKFi+HzH+t/nNVBPpjJxDLXF 9 | dLTJSBIu0bY9uUkeDKtT7QbIMPgokEvdAyfka6PhYlRecsadHQObmDHMEKOFrRu4 10 | CDXzSo2uBfMZSxTTV0VRRHxNKQT/QGN1kPdnsLJ1xXtwaqBIYnLTN2FrqvRKer4+ 11 | molQK8v838q4tOVT0ZKjIFHX+zyebKqJDOsCO+jDnrKIw3VTID6iZ0DNwGp5xNJT 12 | HOLiT7CQM7Lg7fMdQp9xQxrGWJFw26cuvewmBuPdZRdqc8indgT5pq+VIZkLeTAB 13 | XMnf0W35abwIUlfMIvhVZOiaeZzMwzNuV/Qp9GPFAQKBgQDzVpWxefxmTyHCWXz4 14 | 2m+wojoxEBgn6GRdvUbji7vyRRlmGZRNVybwqqxXyCq3RYdIwV55Ihk7O0mmO4g+ 15 | ESuDchZaMxouzXzDKz8jyoG6Gxymd8QJetnA8ctFwUGnTrK3sCxnTAjphGSf1PlT 16 | vdev+f6T/QT0MT7qxUloDC9hAQKBgQC8LCF6OaFlZdi/kKPFcXbYIxFWRCdiun/A 17 | xAL3+UsMllF53aZcWnIsm9rv0sKhPfrTGCp0eLUyinZzjxzdGDeb5hqafWVArXfT 18 | xj4NC5uQVO2w0HGqB0BpZkBCtiyu25Pw0xPvXlcvwCBYcIDvJuf0hwiGBBKPk1b8 19 | LjlkbfoJ7wKBgQDZsU499gmtZYGoIvLAlnpxJNC2b9WMbkTL77bpfmrntJWiV6Pr 20 | BNrbV3TTG0nLp7H9jrB74dt8t++NfZjHHgk1kO0aSLlVwZOp7piP5mzkF7kr291P 21 | Nc505FubzeZ0TN1po3w19TnL3xs+OgPLvPymfBoaPrMd2qiU02Z2ZOBGAQKBgA9V 22 | GTU4VOpKLisNwgpogGKEGPmKfBsTTy2JyyQhb/gKl4Dyioej5wGzgVdhOPKidjmV 23 | EoCDBWCk35ny40swmfdd/HTyGrn2aHkdAhlWBMrx4Jwzn89W3+y2pC3LYkCtK5TH 24 | 3iv25+vAH+KU6CyUYvoNtqgU1N5WBxRtP8frHiCJAoGAOtq0v8K2KKrEI6L/jHhJ 25 | eNZVUWdGQTZx33vvt5lIboxrNyajaBo25LpmmEAKOhgD61ivHJNrxVDkOMKNx2xt 26 | WnLZvQr7M62SdjWFmcFuY/xRbaX6IOW6C9qps3MdtJeFVw9P6z7udXfXHsCEbQvd 27 | YyR0bVDYLxbCAdYjs7PyCA8= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | addPsr4('OpenTok\\', __DIR__.'/OpenTok'); 5 | 6 | -------------------------------------------------------------------------------- /tests/mock/session/create/alwaysarchived: -------------------------------------------------------------------------------- 1 | 2_MX4xNzAxMjYzMX4xMjcuMC4wLjF-V2VkIEZlYiAyNiAxODo1NzoyNCBQU1QgMjAxNH4wLjU0MDU4ODc0fg17012631Wed Feb 26 18:57:24 PST 2014 2 | -------------------------------------------------------------------------------- /tests/mock/session/create/relayed: -------------------------------------------------------------------------------- 1 | 2_MX4xNzAxMjYzMX4xMjcuMC4wLjF-V2VkIEZlYiAyNiAxODo1NzoyNCBQU1QgMjAxNH4wLjU0MDU4ODc0fg17012631Wed Feb 26 18:57:24 PST 2014 2 | -------------------------------------------------------------------------------- /tests/mock/session/create/routed_location-12.34.56.78: -------------------------------------------------------------------------------- 1 | 2_MX4xNzAxMjYzMX4xMjcuMC4wLjF-V2VkIEZlYiAyNiAxODo1NzoyNCBQU1QgMjAxNH4wLjU0MDU4ODc0fg17012631Wed Feb 26 18:57:24 PST 2014 2 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/ARCHIVEID/get: -------------------------------------------------------------------------------- 1 | { 2 | "createdAt" : 1394394801000, 3 | "duration" : 199, 4 | "id" : "063e72a4-64b4-43c8-9da5-eca071daab89", 5 | "name" : "showtime", 6 | "maxBitrate": 2000000, 7 | "partnerId" : 854511, 8 | "reason" : "", 9 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 10 | "size" : 27548485, 11 | "status" : "available", 12 | "url" : "http://tokbox.com.archive2.s3.amazonaws.com/854511%2F063e72a4-64b4-43c8-9da5-eca071daab89%2Farchive.mp4?Expires=1394396270&AWSAccessKeyId=AKIAI6LQCPIXYVWCQV6Q&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXX" 13 | } 14 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/ARCHIVEID/get-expired: -------------------------------------------------------------------------------- 1 | { 2 | "createdAt" : 1394394801000, 3 | "duration" : 199, 4 | "id" : "063e72a4-64b4-43c8-9da5-eca071daab89", 5 | "name" : "showtime", 6 | "partnerId" : 854511, 7 | "reason" : "", 8 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 9 | "size" : 27548485, 10 | "status" : "expired", 11 | "url" : null 12 | } 13 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/ARCHIVEID/stop: -------------------------------------------------------------------------------- 1 | { 2 | "createdAt" : 1394394801000, 3 | "duration" : 0, 4 | "id" : "063e72a4-64b4-43c8-9da5-eca071daab89", 5 | "name" : "showtime", 6 | "partnerId" : 854511, 7 | "reason" : "", 8 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 9 | "size" : 0, 10 | "status" : "stopped", 11 | "url" : null 12 | } 13 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/get: -------------------------------------------------------------------------------- 1 | { 2 | "count" : 2, 3 | "items" : [ { 4 | "createdAt" : 1394396753000, 5 | "duration" : 24, 6 | "id" : "b8f64de1-e218-4091-9544-4cbf369fc238", 7 | "name" : "showtime again", 8 | "partnerId" : 854511, 9 | "reason" : "", 10 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 11 | "size" : 2227849, 12 | "status" : "available", 13 | "url" : "http://tokbox.com.archive2.s3.amazonaws.com/854511%2Fb8f64de1-e218-4091-9544-4cbf369fc238%2Farchive.mp4?Expires=1394397391&AWSAccessKeyId=AKIAI6LQCPIXYVWCQV6Q&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 14 | }, { 15 | "createdAt" : 1394321113000, 16 | "duration" : 1294, 17 | "id" : "832641bf-5dbf-41a1-ad94-fea213e59a92", 18 | "name" : "showtime", 19 | "partnerId" : 854511, 20 | "reason" : "", 21 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 22 | "size" : 42165242, 23 | "status" : "available", 24 | "url" : "http://tokbox.com.archive2.s3.amazonaws.com/854511%2F832641bf-5dbf-41a1-ad94-fea213e59a92%2Farchive.mp4?Expires=1394397391&AWSAccessKeyId=AKIAI6LQCPIXYVWCQV6Q&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 25 | } ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/get_second: -------------------------------------------------------------------------------- 1 | { 2 | "count" : 1, 3 | "items" : [ { 4 | "createdAt" : 1394321113000, 5 | "duration" : 1294, 6 | "id" : "832641bf-5dbf-41a1-ad94-fea213e59a92", 7 | "name" : "showtime", 8 | "partnerId" : 854511, 9 | "reason" : "", 10 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 11 | "size" : 42165242, 12 | "status" : "available", 13 | "url" : "http://tokbox.com.archive2.s3.amazonaws.com/854511%2F832641bf-5dbf-41a1-ad94-fea213e59a92%2Farchive.mp4?Expires=1394397391&AWSAccessKeyId=AKIAI6LQCPIXYVWCQV6Q&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 14 | } ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/get_third: -------------------------------------------------------------------------------- 1 | { 2 | "count" : 2, 3 | "items" : [ { 4 | "createdAt" : 1394396753000, 5 | "duration" : 24, 6 | "id" : "b8f64de1-e218-4091-9544-4cbf369fc238", 7 | "name" : "showtime again", 8 | "partnerId" : 12345678, 9 | "reason" : "", 10 | "sessionId" : "1_MX4xMjM0NTY3OH4-VGh1IEZlYiAyNyAwNDozODozMSBQU1QgMjAxNH4wLjI0NDgyMjI", 11 | "size" : 2227849, 12 | "status" : "available", 13 | "url" : "http://tokbox.com.archive2.s3.amazonaws.com/854511%2Fb8f64de1-e218-4091-9544-4cbf369fc238%2Farchive.mp4?Expires=1394397391&AWSAccessKeyId=AKIAI6LQCPIXYVWCQV6Q&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 14 | }, { 15 | "createdAt" : 1394321113000, 16 | "duration" : 1294, 17 | "id" : "832641bf-5dbf-41a1-ad94-fea213e59a92", 18 | "name" : "showtime", 19 | "partnerId" : 12345678, 20 | "reason" : "", 21 | "sessionId" : "1_MX4xMjM0NTY3OH4-VGh1IEZlYiAyNyAwNDozODozMSBQU1QgMjAxNH4wLjI0NDgyMjI", 22 | "size" : 42165242, 23 | "status" : "available", 24 | "url" : "http://tokbox.com.archive2.s3.amazonaws.com/854511%2F832641bf-5dbf-41a1-ad94-fea213e59a92%2Farchive.mp4?Expires=1394397391&AWSAccessKeyId=AKIAI6LQCPIXYVWCQV6Q&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 25 | } ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/manual_mode_session: -------------------------------------------------------------------------------- 1 | { 2 | "createdAt" : 1394321113584, 3 | "duration" : 0, 4 | "id" : "832641bf-5dbf-41a1-ad94-fea213e59a92", 5 | "name" : null, 6 | "partnerId" : 12345678, 7 | "reason" : "", 8 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 9 | "size" : 0, 10 | "status" : "started", 11 | "url" : null, 12 | "hasVideo" : true, 13 | "hasAudio" : true, 14 | "outputMode": "composed", 15 | "streamMode": "manual" 16 | } 17 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/session: -------------------------------------------------------------------------------- 1 | { 2 | "createdAt" : 1394321113584, 3 | "duration" : 0, 4 | "id" : "832641bf-5dbf-41a1-ad94-fea213e59a92", 5 | "name" : null, 6 | "maxBitrate": 2000000, 7 | "partnerId" : 12345678, 8 | "reason" : "", 9 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 10 | "size" : 0, 11 | "status" : "started", 12 | "url" : null, 13 | "hasVideo" : true, 14 | "hasAudio" : true, 15 | "outputMode" : "composed", 16 | "streamMode" : "auto" 17 | } 18 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/session_hasVideo-false: -------------------------------------------------------------------------------- 1 | { 2 | "createdAt" : 1394321113584, 3 | "duration" : 0, 4 | "id" : "832641bf-5dbf-41a1-ad94-fea213e59a92", 5 | "name" : null, 6 | "partnerId" : 12345678, 7 | "reason" : "", 8 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 9 | "size" : 0, 10 | "status" : "started", 11 | "url" : null, 12 | "hasVideo" : false, 13 | "hasAudio" : true 14 | } 15 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/session_name-showtime: -------------------------------------------------------------------------------- 1 | { 2 | "createdAt" : 1394321113584, 3 | "duration" : 0, 4 | "id" : "832641bf-5dbf-41a1-ad94-fea213e59a92", 5 | "name" : "showtime", 6 | "partnerId" : 12345678, 7 | "reason" : "", 8 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 9 | "size" : 0, 10 | "status" : "started", 11 | "url" : null 12 | } 13 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/session_outputMode-individual: -------------------------------------------------------------------------------- 1 | { 2 | "createdAt" : 1394321113584, 3 | "duration" : 0, 4 | "id" : "832641bf-5dbf-41a1-ad94-fea213e59a92", 5 | "name" : null, 6 | "partnerId" : 12345678, 7 | "reason" : "", 8 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 9 | "size" : 0, 10 | "status" : "started", 11 | "url" : null, 12 | "hasVideo" : true, 13 | "hasAudio" : true, 14 | "outputMode" : "individual" 15 | } 16 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/session_resolution-hd: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "832641bf-5dbf-41a1-ad94-fea213e59a92", 3 | "createdAt" : 1394321113584, 4 | "duration" : 0, 5 | "name" : null, 6 | "partnerId" : 12345678, 7 | "reason" : "", 8 | "resolution" : "1280x720", 9 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 10 | "size" : 0, 11 | "status" : "started", 12 | "url" : null 13 | } 14 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/archive/session_resolution-sd: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "832641bf-5dbf-41a1-ad94-fea213e59a92", 3 | "createdAt" : 1394321113584, 4 | "duration" : 0, 5 | "name" : null, 6 | "partnerId" : 12345678, 7 | "reason" : "", 8 | "resolution" : "640x480", 9 | "sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-", 10 | "size" : 0, 11 | "status" : "started", 12 | "url" : null 13 | } 14 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/broadcast/BROADCASTID/get: -------------------------------------------------------------------------------- 1 | { 2 | "id":"6706b658-2eba-42cc-b4d2-7d01a104d182", 3 | "sessionId":"2_MX44NTQ1MTF-fjE0NzI0MzU2MDUyMjN-eVgwNFJhZmR6MjdockFHanpxNzBXaEFXfn4", 4 | "projectId":854511, 5 | "createdAt":1472435660275, 6 | "broadcastUrls":null, 7 | "updatedAt":1472435660275, 8 | "resolution": "640x480", 9 | "status":"stopped", 10 | "partnerId":854511 11 | } 12 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/broadcast/BROADCASTID/layout/get: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/OpenTok-PHP-SDK/0e5b31608cb14664ce529321287032649f8c50bc/tests/mock/v2/project/APIKEY/broadcast/BROADCASTID/layout/get -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/broadcast/BROADCASTID/layout/type-custom: -------------------------------------------------------------------------------- 1 | { 2 | "id":"f3621dc5-7793-4cdb-9a4a-242e048542db", 3 | "sessionId":"2_MX44NTQ1MTF-fjE0NzI0MzU2MDUyMjN-eVgwNFJhZmR6MjdockFHanpxNzBXaEFXfn4", 4 | "projectId":854511, 5 | "createdAt":1472445034077, 6 | "broadcastUrls":{ 7 | "hls":"https://cdn-broadcast001-dub.tokbox.com/29932/29932_f3621dc5-7793-4cdb-9a4a-242e048542db.smil/playlist.m3u8" 8 | }, 9 | "updatedAt":1472445034077, 10 | "status":"started", 11 | "partnerId":854511 12 | } 13 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/broadcast/BROADCASTID/layout/type-pip: -------------------------------------------------------------------------------- 1 | { 2 | "id":"f3621dc5-7793-4cdb-9a4a-242e048542db", 3 | "sessionId":"2_MX44NTQ1MTF-fjE0NzI0MzU2MDUyMjN-eVgwNFJhZmR6MjdockFHanpxNzBXaEFXfn4", 4 | "projectId":854511, 5 | "createdAt":1472445034077, 6 | "broadcastUrls":{ 7 | "hls":"https://cdn-broadcast001-dub.tokbox.com/29932/29932_f3621dc5-7793-4cdb-9a4a-242e048542db.smil/playlist.m3u8" 8 | }, 9 | "updatedAt":1472445034077, 10 | "status":"started", 11 | "partnerId":854511 12 | } 13 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/broadcast/BROADCASTID/start_default: -------------------------------------------------------------------------------- 1 | { 2 | "id":"6706b658-2eba-42cc-b4d2-7d01a104d182", 3 | "sessionId":"2_MX44NTQ1MTF-fjE0NzI0MzU2MDUyMjN-eVgwNFJhZmR6MjdockFHanpxNzBXaEFXfn4", 4 | "projectId":854511, 5 | "createdAt":1472435659497, 6 | "broadcastUrls":{ 7 | "hls":"https://cdn-broadcast001-dub.tokbox.com/29908/29908_6706b658-2eba-42cc-b4d2-7d01a104d182.smil/playlist.m3u8", 8 | "hlsStatus": "ready", 9 | "rtmp": { 10 | "foo": { 11 | "serverUrl": "rtmp://myfooserver/myfooapp", 12 | "streamName": "myfoostreamname", 13 | "status": "live" 14 | }, 15 | "bar": { 16 | "serverUrl": "rtmp://mybarserver/mybarapp", 17 | "streamName": "mybarstreamname", 18 | "status": "offline" 19 | } 20 | } 21 | }, 22 | "settings": {"hls": {"lowLatency": false, "dvr": false}}, 23 | "updatedAt":1472435660268, 24 | "status":"started", 25 | "partnerId":854511, 26 | "maxDuration":5400, 27 | "maxBitRate": 2000000, 28 | "resolution": "1280x720", 29 | "streamMode": "auto", 30 | "hasAudio": true, 31 | "hasVideo": true 32 | } 33 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/broadcast/BROADCASTID/start_dvr: -------------------------------------------------------------------------------- 1 | { 2 | "id":"6706b658-2eba-42cc-b4d2-7d01a104d182", 3 | "sessionId":"2_MX44NTQ1MTF-fjE0NzI0MzU2MDUyMjN-eVgwNFJhZmR6MjdockFHanpxNzBXaEFXfn4", 4 | "projectId":854511, 5 | "createdAt":1472435659497, 6 | "broadcastUrls":{ 7 | "hls":"https://cdn-broadcast001-dub.tokbox.com/29908/29908_6706b658-2eba-42cc-b4d2-7d01a104d182.smil/playlist.m3u8", 8 | "rtmp": { 9 | "foo": { 10 | "serverUrl": "rtmp://myfooserver/myfooapp", 11 | "streamName": "myfoostreamname" 12 | }, 13 | "bar": { 14 | "serverUrl": "rtmp://mybarserver/mybarapp", 15 | "streamName": "mybarstreamname" 16 | } 17 | } 18 | }, 19 | "settings": {"hls": {"lowLatency": false, "dvr": true}}, 20 | "updatedAt":1472435660268, 21 | "status":"started", 22 | "partnerId":854511, 23 | "maxDuration":5400, 24 | "maxBitRate": 2000000, 25 | "resolution": "1280x720", 26 | "streamMode": "auto", 27 | "hasAudio": true, 28 | "hasVideo": true 29 | } 30 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/broadcast/BROADCASTID/start_ll: -------------------------------------------------------------------------------- 1 | { 2 | "id":"6706b658-2eba-42cc-b4d2-7d01a104d182", 3 | "sessionId":"2_MX44NTQ1MTF-fjE0NzI0MzU2MDUyMjN-eVgwNFJhZmR6MjdockFHanpxNzBXaEFXfn4", 4 | "projectId":854511, 5 | "createdAt":1472435659497, 6 | "broadcastUrls":{ 7 | "hls":"https://cdn-broadcast001-dub.tokbox.com/29908/29908_6706b658-2eba-42cc-b4d2-7d01a104d182.smil/playlist.m3u8", 8 | "hlsStatus": "ready", 9 | "rtmp": { 10 | "foo": { 11 | "serverUrl": "rtmp://myfooserver/myfooapp", 12 | "streamName": "myfoostreamname", 13 | "status": "live" 14 | }, 15 | "bar": { 16 | "serverUrl": "rtmp://mybarserver/mybarapp", 17 | "streamName": "mybarstreamname", 18 | "status": "offline" 19 | } 20 | } 21 | }, 22 | "settings": {"hls": {"lowLatency": true, "dvr": false}}, 23 | "updatedAt":1472435660268, 24 | "status":"started", 25 | "partnerId":854511, 26 | "maxDuration":5400, 27 | "maxBitRate": 2000000, 28 | "resolution": "1280x720", 29 | "streamMode": "auto", 30 | "hasAudio": true, 31 | "hasVideo": true 32 | } 33 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/broadcast/BROADCASTID/stop: -------------------------------------------------------------------------------- 1 | { 2 | "id":"6706b658-2eba-42cc-b4d2-7d01a104d182", 3 | "sessionId":"2_MX44NTQ1MTF-fjE0NzI0MzU2MDUyMjN-eVgwNFJhZmR6MjdockFHanpxNzBXaEFXfn4", 4 | "projectId":854511, 5 | "createdAt":1472435660275, 6 | "broadcastUrls":null, 7 | "updatedAt":1472435660275, 8 | "resolution": "640x480", 9 | "status":"stopped", 10 | "partnerId":854511 11 | } 12 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/broadcast/session_layout-bestfit: -------------------------------------------------------------------------------- 1 | { 2 | "id":"6706b658-2eba-42cc-b4d2-7d01a104d182", 3 | "sessionId":"2_MX44NTQ1MTF-fjE0NzI0MzU2MDUyMjN-eVgwNFJhZmR6MjdockFHanpxNzBXaEFXfn4", 4 | "projectId":854511, 5 | "createdAt":1472435659497, 6 | "broadcastUrls":{ 7 | "hls":"https://cdn-broadcast001-dub.tokbox.com/29908/29908_6706b658-2eba-42cc-b4d2-7d01a104d182.smil/playlist.m3u8", 8 | "rtmp": { 9 | "foo": { 10 | "serverUrl": "rtmp://myfooserver/myfooapp", 11 | "streamName": "myfoostreamname" 12 | }, 13 | "bar": { 14 | "serverUrl": "rtmp://mybarserver/mybarapp", 15 | "streamName": "mybarstreamname" 16 | } 17 | } 18 | }, 19 | "updatedAt":1472435660268, 20 | "status":"started", 21 | "partnerId":854511, 22 | "maxDuration":5400, 23 | "maxBitRate": 2000000, 24 | "resolution": "1280x720", 25 | "streamMode": "auto" 26 | } 27 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/broadcast/session_manual_stream: -------------------------------------------------------------------------------- 1 | { 2 | "id":"6706b658-2eba-42cc-b4d2-7d01a104d182", 3 | "sessionId":"2_MX44NTQ1MTF-fjE0NzI0MzU2MDUyMjN-eVgwNFJhZmR6MjdockFHanpxNzBXaEFXfn4", 4 | "projectId":854511, 5 | "createdAt":1472435659497, 6 | "broadcastUrls":{ 7 | "hls":"https://cdn-broadcast001-dub.tokbox.com/29908/29908_6706b658-2eba-42cc-b4d2-7d01a104d182.smil/playlist.m3u8", 8 | "rtmp": { 9 | "foo": { 10 | "serverUrl": "rtmp://myfooserver/myfooapp", 11 | "streamName": "myfoostreamname" 12 | }, 13 | "bar": { 14 | "serverUrl": "rtmp://mybarserver/mybarapp", 15 | "streamName": "mybarstreamname" 16 | } 17 | } 18 | }, 19 | "updatedAt":1472435660268, 20 | "status":"started", 21 | "partnerId":854511, 22 | "maxDuration":5400, 23 | "maxBitRate": 2000000, 24 | "resolution": "1280x720", 25 | "streamMode": "manual" 26 | } 27 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/connect: -------------------------------------------------------------------------------- 1 | { 2 | "id": "063e72a4-64b4-43c8-9da5-eca071daab89", 3 | "connectionId": "7aebb3a4-3d86-4962-b317-afb73e05439d" 4 | } -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/dial: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "1bae377f-d620-4e58-86c0-5a37364eec0c", 3 | "connectionId" : "da9cb410-e29b-4c2d-ab9e-fe65bf83fcaf", 4 | "streamId" : "e6f13213-22cb-45de-b71d-af7cef329954" 5 | } 6 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/dialForceMute: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "1bae377f-d620-4e58-86c0-5a37364eec0c", 3 | "connectionId" : "da9cb410-e29b-4c2d-ab9e-fe65bf83fcaf", 4 | "streamId" : "e6f13213-22cb-45de-b71d-af7cef329954" 5 | } 6 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/render/render_get: -------------------------------------------------------------------------------- 1 | { 2 | "id":"80abaf0d-25a3-4efc-968f-6268d620668d", 3 | "sessionId":"1_MX4yNzA4NjYxMn5-MTU0NzA4MDUyMTEzNn5sOXU5ZnlWYXplRnZGblV4RUo3dXJpZk1-fg", 4 | "projectId":"27086612", 5 | "createdAt":1547080532099, 6 | "updatedAt":1547080532199, 7 | "url": "https://webapp.customer.com", 8 | "resolution": "480x640", 9 | "status":"failed", 10 | "reason":"Could not load URL" 11 | } -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/render/render_list: -------------------------------------------------------------------------------- 1 | { 2 | "count":2, 3 | "items":[ 4 | { 5 | "id":"80abaf0d-25a3-4efc-968f-6268d620668d", 6 | "sessionId":"1_MX4yNzA4NjYxMn5-MTU0NzA4MDUyMTEzNn5sOXU5ZnlWYXplRnZGblV4RUo3dXJpZk1-fg", 7 | "projectId":"27086612", 8 | "createdAt":1547080532099, 9 | "updatedAt":1547080532099, 10 | "url": "https://webapp.customer.com", 11 | "resolution": "1280x720", 12 | "status": "started", 13 | "streamId": "d2334b35690a92f78945" 14 | }, 15 | { 16 | "id":"d95f6496-df6e-4f49-86d6-832e00303602", 17 | "sessionId":"2_MX4yNzA4NjYxMn5-MTU0NzA4MDUwMDc2MH5STWRiSE1jZjVoV3lBQU9nN2JuNElUV3V-fg", 18 | "projectId":"27086612", 19 | "createdAt":1547080511760, 20 | "updatedAt":1547080518965, 21 | "url": "https://webapp2.customer.com", 22 | "resolution": "1280x720", 23 | "status":"stopped", 24 | "streamId": "d2334b35690a92f78945", 25 | "reason":"Maximum duration exceeded" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/render/render_start: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1248e7070b81464c9789f46ad10e7764", 3 | "sessionId": "2_MX4xMDBfjE0Mzc2NzY1NDgwMTJ-TjMzfn4", 4 | "projectId": "e2343f23456g34709d2443a234", 5 | "createdAt": 1437676551000, 6 | "updatedAt": 1437676551000, 7 | "url": "https://webapp.customer.com", 8 | "resolution": "1280x720", 9 | "status": "started", 10 | "streamId": "e32445b743678c98230f238" 11 | } -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/session/SESSIONID/caption-start: -------------------------------------------------------------------------------- 1 | { 2 | "captionsId": "7c0680fc-6274-4de5-a66f-d0648e8d3ac2" 3 | } -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/session/SESSIONID/caption-stop: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/OpenTok-PHP-SDK/0e5b31608cb14664ce529321287032649f8c50bc/tests/mock/v2/project/APIKEY/session/SESSIONID/caption-stop -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/session/SESSIONID/mute: -------------------------------------------------------------------------------- 1 | { 2 | "code" : 200, 3 | "message" : "OK" 4 | } 5 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/session/SESSIONID/play-dtmf: -------------------------------------------------------------------------------- 1 | { 2 | "code" : 200, 3 | "message" : "OK" 4 | } 5 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/session/SESSIONID/play-dtmf-400: -------------------------------------------------------------------------------- 1 | { 2 | "code" : 400, 3 | "message" : "Invalid DTMF Digits" 4 | } 5 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/session/SESSIONID/stream/STREAMID/get: -------------------------------------------------------------------------------- 1 | { 2 | "id":"8b732909-0a06-46a2-8ea8-074e64d43422", 3 | "videoType":"camera", 4 | "name":"no name", 5 | "layoutClassList":["foo"] 6 | } 7 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/session/SESSIONID/stream/STREAMID/layoutClassList: -------------------------------------------------------------------------------- 1 | { 2 | "id":"STREAMID", 3 | "videoType":"camera", 4 | "name":"", 5 | "layoutClassList":["foo","bar"] 6 | } 7 | -------------------------------------------------------------------------------- /tests/mock/v2/project/APIKEY/session/SESSIONID/stream/get: -------------------------------------------------------------------------------- 1 | { 2 | "count": 2 3 | "items": [ 4 | { 5 | "id": "8b732909-0a06-46a2-8ea8-074e64d43422", 6 | "videoType": "camera", 7 | "name": "item 1", 8 | "layoutClassList": ["full"] 9 | }, 10 | { 11 | "id": "ab732909-0a06-46a2-8ea8-074e64d43412", 12 | "videoType": "screen", 13 | "name": "item 2", 14 | "layoutClassList": ["full"] 15 | }, 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tools/stub.php: -------------------------------------------------------------------------------- 1 |