├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── scripts ├── karma.conf.js ├── postcss.config.js └── rollup.config.js ├── src ├── components │ ├── ResolutionMenuButton.js │ └── ResolutionMenuItem.js ├── const │ ├── CALLBACKS.js │ └── COMMANDS.js ├── plugin.js └── plugin.scss └── test └── plugin.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | should-skip: 7 | continue-on-error: true 8 | runs-on: ubuntu-22.04 9 | # Map a step output to a job output 10 | outputs: 11 | should-skip-job: ${{steps.skip-check.outputs.should_skip}} 12 | steps: 13 | - id: skip-check 14 | uses: fkirc/skip-duplicate-actions@v2.1.0 15 | with: 16 | github_token: ${{github.token}} 17 | 18 | ci: 19 | needs: should-skip 20 | if: ${{needs.should-skip.outputs.should-skip-job != 'true' || github.ref == 'refs/heads/main'}} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: [ubuntu-22.04] 25 | test-type: ['unit'] 26 | env: 27 | BROWSER_STACK_USERNAME: ${{secrets.BROWSER_STACK_USERNAME}} 28 | BROWSER_STACK_ACCESS_KEY: ${{secrets.BROWSER_STACK_ACCESS_KEY}} 29 | CI_TEST_TYPE: ${{matrix.test-type}} 30 | runs-on: ${{matrix.os}} 31 | steps: 32 | - name: checkout code 33 | uses: actions/checkout@v2 34 | 35 | - name: read node version from .nvmrc 36 | run: echo ::set-output name=NVMRC::$(cat .nvmrc) 37 | shell: bash 38 | id: nvm 39 | 40 | - name: update apt cache on linux w/o browserstack 41 | run: sudo apt-get update 42 | 43 | - name: install ffmpeg/pulseaudio for firefox on linux w/o browserstack 44 | run: sudo apt-get install ffmpeg pulseaudio 45 | 46 | - name: start pulseaudio for firefox on linux w/o browserstack 47 | run: pulseaudio -D 48 | 49 | - name: setup node 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version: '${{steps.nvm.outputs.NVMRC}}' 53 | cache: npm 54 | 55 | # turn off the default setup-node problem watchers... 56 | - run: echo "::remove-matcher owner=eslint-compact::" 57 | - run: echo "::remove-matcher owner=eslint-stylish::" 58 | - run: echo "::remove-matcher owner=tsc::" 59 | 60 | - name: npm install 61 | run: npm i --prefer-offline --no-audit 62 | 63 | - name: run npm test 64 | uses: GabrielBB/xvfb-action@v1 65 | with: 66 | run: npm run test 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | Thumbs.db 3 | ehthumbs.db 4 | Desktop.ini 5 | .DS_Store 6 | ._* 7 | 8 | # Editors 9 | *~ 10 | *.swp 11 | *.tmproj 12 | *.tmproject 13 | *.sublime-* 14 | .idea/ 15 | .project/ 16 | .settings/ 17 | .vscode/ 18 | 19 | # Logs 20 | logs 21 | *.log 22 | npm-debug.log* 23 | 24 | # Dependency directories 25 | bower_components/ 26 | node_modules/ 27 | 28 | # Build-related directories 29 | dist/ 30 | es/ 31 | cjs/ 32 | docs/api/ 33 | test/dist/ 34 | .eslintcache 35 | .yo-rc.json 36 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Intentionally left blank, so that npm does not ignore anything by default, 2 | # but relies on the package.json "files" array to explicitly define what ends 3 | # up in the package. 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-media/videojs-webrtc-plugin/e5e94e54f7a17e919920581e0ed92cea1d4474c5/CHANGELOG.md -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | We welcome contributions from everyone! 4 | 5 | ## Getting Started 6 | 7 | Make sure you have Node.js 8 or higher and npm installed. 8 | 9 | 1. Fork this repository and clone your fork 10 | 1. Install dependencies: `npm install` 11 | 1. Run a development server: `npm start` 12 | 13 | ### Making Changes 14 | 15 | Refer to the [video.js plugin conventions][conventions] for more detail on best practices and tooling for video.js plugin authorship. 16 | 17 | When you've made your changes, push your commit(s) to your fork and issue a pull request against the original repository. 18 | 19 | ### Running Tests 20 | 21 | Testing is a crucial part of any software project. For all but the most trivial changes (typos, etc) test cases are expected. Tests are run in actual browsers using [Karma][karma]. 22 | 23 | - In all available and supported browsers: `npm test` 24 | - In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc. 25 | - While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local] 26 | 27 | 28 | [karma]: http://karma-runner.github.io/ 29 | [local]: http://localhost:9999/test/ 30 | [conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) ForaSoft 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # videojs-webrtc-plugin 2 | 3 | Plugin for viewing streams located on the ant-media server. There is also a function to change the resolution of the stream 4 | 5 | ## Table of Contents 6 | 7 | 8 | 9 | 10 | - [Quick Start](#quick-start) 11 | - [Installation](#installation) 12 | - [Issues](#issues) 13 | - [Usage](#usage) 14 | - [Source Object](#source-object) 15 | - [**streamUrl**](#streamurl) 16 | - [**iceServers**](#iceservers) 17 | - [` 98 | 99 | 100 |
101 | 106 |
107 | 110 | ``` 111 | 112 | ### Browserify/CommonJS 113 | 114 | When using with Browserify, install videojs-webrtc-plugin via npm and `require` the plugin as you would any other module. 115 | 116 | ```js 117 | var videojs = require('video.js'); 118 | 119 | // The actual plugin function is exported by this module, but it is also 120 | // attached to the `Player.prototype`; so, there is no need to assign it 121 | // to a variable. 122 | require('videojs-webrtc-plugin'); 123 | 124 | var player = videojs('my-video'); 125 | 126 | player.src({ 127 | src: 'ws://localhost:5080/LiveApp/stream1.webrtc', 128 | iceServers: '[ { "urls": "stun:stun1.l.google.com:19302" } ]' 129 | }); 130 | ``` 131 | 132 | ### RequireJS/AMD 133 | 134 | When using with RequireJS (or another AMD library), get the script in whatever way you prefer and `require` the plugin as you normally would: 135 | 136 | ```js 137 | require(['video.js', 'videojs-webrtc-plugin'], function(videojs) { 138 | var player = videojs('my-video'); 139 | 140 | player.src({ 141 | src: 'ws://localhost:5080/LiveApp/stream1.webrtc', 142 | iceServers: '[ { "urls": "stun:stun1.l.google.com:19302" } ]' 143 | }); 144 | }); 145 | ``` 146 | 147 | ### Handling error-callbacks 148 | 149 | Ant-MediaServer has functionality to handle errors coming from the backend. 150 | To catch an error, you need to subscribe to the event "ant-error": 151 | 152 | ```js 153 | 154 | 155 | 166 | ``` 167 | ## License 168 | 169 | MIT. Copyright (c) Ant Media 170 | 171 | [videojs]: http://videojs.com/ 172 | 173 | ## Issues 174 | 175 | In case of any problem, please create issues at [Ant-Media-Server Repository](https://github.com/ant-media/Ant-Media-Server/issues) 176 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | videojs-webrtc-plugin Demo 6 | 7 | 8 | 46 | 47 | 48 |
49 | 50 |
51 | 52 | 57 | 58 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@antmedia/videojs-webrtc-plugin", 3 | "version": "1.3.3", 4 | "description": "streaming via WebRTC with Ant-MediaServer", 5 | "main": "dist/videojs-webrtc-plugin.cjs.js", 6 | "module": "dist/videojs-webrtc-plugin.es.js", 7 | "browser": "dist/videojs-webrtc-plugin.js", 8 | "generator-videojs-plugin": { 9 | "version": "8.0.0" 10 | }, 11 | "scripts": { 12 | "build": "npm-run-all -s clean -p build:*", 13 | "build-prod": "cross-env-shell NO_TEST_BUNDLE=1 'npm run build'", 14 | "build-test": "cross-env-shell TEST_BUNDLE_ONLY=1 'npm run build'", 15 | "build:css": "postcss -o dist/videojs-webrtc-plugin.css --config scripts/postcss.config.js src/plugin.scss", 16 | "build:js": "rollup -c scripts/rollup.config.js", 17 | "clean": "shx rm -rf ./dist ./test/dist ./cjs ./es && shx mkdir -p ./dist ./test/dist ./cjs ./es", 18 | "docs": "npm-run-all docs:*", 19 | "docs:api": "jsdoc src -r -d docs/api", 20 | "docs:toc": "doctoc --notitle README.md", 21 | "lint": "vjsstandard", 22 | "server": "karma start scripts/karma.conf.js --singleRun=false --auto-watch", 23 | "start": "npm-run-all -p server watch", 24 | "test": "npm-run-all lint build-test && karma start scripts/karma.conf.js", 25 | "posttest": "shx cat test/dist/coverage/text.txt", 26 | "update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s", 27 | "preversion": "npm test", 28 | "version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md", 29 | "watch": "npm-run-all -p watch:*", 30 | "watch:css": "npm run build:css -- -w", 31 | "watch:js": "npm run build:js -- -w", 32 | "prepublishOnly": "npm-run-all build-prod" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/ant-media/videojs-webrtc-plugin" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/ant-media/videojs-webrtc-plugin/issues" 40 | }, 41 | "homepage": "https://github.com/ant-media/videojs-webrtc-plugin#readme", 42 | "engines": { 43 | "node": ">=14", 44 | "npm": ">=6" 45 | }, 46 | "keywords": [ 47 | "videojs", 48 | "videojs-plugin" 49 | ], 50 | "author": "AntMedia", 51 | "license": "MIT", 52 | "vjsstandard": { 53 | "ignore": [ 54 | "es", 55 | "cjs", 56 | "dist", 57 | "docs", 58 | "test/dist" 59 | ] 60 | }, 61 | "files": [ 62 | "CONTRIBUTING.md", 63 | "cjs/", 64 | "dist/", 65 | "docs/", 66 | "es/", 67 | "index.html", 68 | "scripts/", 69 | "src/", 70 | "test/" 71 | ], 72 | "husky": { 73 | "hooks": { 74 | "pre-commit": "lint-staged" 75 | } 76 | }, 77 | "lint-staged": { 78 | "*.js": "vjsstandard --fix", 79 | "README.md": "doctoc --notitle" 80 | }, 81 | "dependencies": { 82 | "@antmedia/webrtc_adaptor": "^2.11.3", 83 | "global": "^4.4.0", 84 | "video.js": "^8" 85 | }, 86 | "devDependencies": { 87 | "@babel/runtime": "^7.14.0", 88 | "@videojs/generator-helpers": "~3.0.0", 89 | "jsdoc": "~3.6.6", 90 | "karma": "^6.3.2", 91 | "postcss": "^8.2.13", 92 | "postcss-cli": "^8.3.1", 93 | "rollup": "^2.46.0", 94 | "sinon": "^9.1.0", 95 | "videojs-generate-karma-config": "~8.0.0", 96 | "videojs-generate-postcss-config": "~3.0.0", 97 | "videojs-generate-rollup-config": "~7.0.1", 98 | "videojs-generator-verify": "~4.0.0", 99 | "videojs-standard": "^8.0.4" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /scripts/karma.conf.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-karma-config'); 2 | 3 | module.exports = function(config) { 4 | 5 | // see https://github.com/videojs/videojs-generate-karma-config 6 | // for options 7 | const options = {}; 8 | 9 | config = generate(config, options); 10 | 11 | // any other custom stuff not supported by options here! 12 | }; 13 | -------------------------------------------------------------------------------- /scripts/postcss.config.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-postcss-config'); 2 | 3 | module.exports = function(context) { 4 | const result = generate({}, context); 5 | 6 | // do custom stuff here 7 | 8 | return result; 9 | }; 10 | -------------------------------------------------------------------------------- /scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | const generate = require('videojs-generate-rollup-config'); 2 | 3 | // see https://github.com/videojs/videojs-generate-rollup-config 4 | // for options 5 | const options = {}; 6 | const config = generate(options); 7 | 8 | // Add additonal builds/customization here! 9 | 10 | // export the builds to rollup 11 | export default Object.values(config.builds); 12 | -------------------------------------------------------------------------------- /src/components/ResolutionMenuButton.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | import ResolutionMenuItem from './ResolutionMenuItem'; 3 | 4 | const MenuButton = videojs.getComponent('MenuButton'); 5 | 6 | class ResolutionMenuButton extends MenuButton { 7 | 8 | constructor(player, options) { 9 | super(player, options); 10 | } 11 | 12 | createEl() { 13 | return videojs.dom.createEl('div', { 14 | className: 'vjs-http-source-selector vjs-menu-button vjs-menu-button-popup vjs-control vjs-button' 15 | }); 16 | } 17 | 18 | buildCSSClass() { 19 | return `${super.buildCSSClass()} vjs-icon-cog`; 20 | } 21 | 22 | update() { 23 | return super.update(); 24 | } 25 | 26 | createItems() { 27 | const menuItems = []; 28 | const levels = [{ 29 | label: 'auto', 30 | value: 0 31 | }, ...this.player().resolutions]; 32 | 33 | for (let i = 0; i < levels.length; i++) { 34 | menuItems.push(new ResolutionMenuItem(this.player_, { 35 | label: levels[i].label, 36 | value: levels[i].value, 37 | selected: levels[i].value === this.player().selectedResolution, 38 | plugin: this.options().plugin, 39 | streamName: this.options().streamName 40 | })); 41 | } 42 | 43 | return menuItems; 44 | } 45 | } 46 | 47 | export default ResolutionMenuButton; 48 | -------------------------------------------------------------------------------- /src/components/ResolutionMenuItem.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | const MenuItem = videojs.getComponent('MenuItem'); 3 | const Component = videojs.getComponent('Component'); 4 | 5 | class ResolutionMenuItem extends MenuItem { 6 | 7 | constructor(player, options) { 8 | options.selectable = true; 9 | options.multiSelectable = false; 10 | super(player, options); 11 | } 12 | 13 | handleClick() { 14 | this.options().plugin.changeStreamQuality(this.options().value); 15 | } 16 | 17 | } 18 | 19 | Component.registerComponent('ResolutionMenuItem', ResolutionMenuItem); 20 | export default ResolutionMenuItem; 21 | -------------------------------------------------------------------------------- /src/const/CALLBACKS.js: -------------------------------------------------------------------------------- 1 | export const ANT_CALLBACKS = { 2 | INITIALIZED: 'initialized', 3 | PLAY_STARTED: 'play_started', 4 | PLAY_FINISHED: 'play_finished', 5 | CLOSED: 'closed', 6 | STREAM_INFORMATION: 'streamInformation', 7 | RESOLUTION_CHANGE_INFO: 'resolutionChangeInfo', 8 | ICE_CONNECTION_STATE_CHANGED: 'ice_connection_state_changed', 9 | DATA_RECEIVED: 'data_received', 10 | DATACHANNEL_NOT_OPEN: 'data_channel_not_open', 11 | NEW_TRACK_AVAILABLE: 'newTrackAvailable' 12 | }; 13 | -------------------------------------------------------------------------------- /src/const/COMMANDS.js: -------------------------------------------------------------------------------- 1 | export const COMMANDS = { 2 | TAKE_CANDIDATE: 'takeCandidate', 3 | TAKE_CONFIGURATION: 'takeConfiguration', 4 | PLAY: 'play', 5 | STOP: 'stop', 6 | GET_STREAM_INFO: 'getStreamInfo', 7 | PEER_MESSAGE_COMMAND: 'peerMessageCommand', 8 | FORCE_STREAM_QUALITY: 'forceStreamQuality', 9 | ERROR: 'error', 10 | NOTIFICATION: 'notification', 11 | STREAM_INFORMATION: 'streamInformation', 12 | PING: 'ping', 13 | PONG: 'pong', 14 | TRACK_LIST: 'trackList' 15 | }; 16 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | import {ANT_CALLBACKS} from './const/CALLBACKS'; 3 | import ResolutionMenuButton from './components/ResolutionMenuButton'; 4 | import ResolutionMenuItem from './components/ResolutionMenuItem'; 5 | import { WebRTCAdaptor } from '@antmedia/webrtc_adaptor'; 6 | 7 | // Default options for the plugin. 8 | const defaults = { 9 | sdpConstraints: { OfferToReceiveAudio: true, OfferToReceiveVideo: true }, 10 | mediaConstraints: { video: false, audio: false } 11 | }; 12 | 13 | // const Component = videojs.getComponent('Component'); 14 | /** 15 | * An advanced Video.js plugin for playing WebRTC stream from Ant Media Server 16 | * 17 | * Test Scenario #1 18 | * 1. Publish a stream from a WebRTC endpoint to Ant Media Server 19 | * 2. Play the stream with WebRTC 20 | * 3. Restart publishing the stream 21 | * 4. It should play automatically 22 | * 23 | * Test Scenario #2 24 | * 1. Publish a stream from a WebRTC endpoint to Ant Media Server 25 | * 2. Let the server return error(highresourceusage, etc.) 26 | * 3. WebSocket should be disconnected and play should try again 27 | * 28 | * Test Scenario #3 29 | * 1. Show error message if packet lost and jitter and RTT is high 30 | */ 31 | class WebRTCHandler { 32 | 33 | /** 34 | * Create a WebRTC source handler instance. 35 | * 36 | * @param {Object} source 37 | * Source object that is given in the DOM, includes the stream URL 38 | * 39 | * @param {Object} [options] 40 | * Options include: 41 | * ICE Server 42 | * Tokens 43 | * Subscriber ID 44 | * Subscriber code 45 | */ 46 | constructor(source, tech, options) { 47 | this.player = videojs(options.playerId); 48 | 49 | if (!this.player.hasOwnProperty('sendDataViaWebRTC')) { 50 | Object.defineProperty(this.player, 'sendDataViaWebRTC', { 51 | value: (data) => { 52 | this.webRTCAdaptor.sendData(this.source.streamName, data); 53 | } 54 | }); 55 | } 56 | 57 | this.isPlaying = false; 58 | this.disposed = false; 59 | 60 | this.initiateWebRTCAdaptor(source, options); 61 | this.player.ready(() => { 62 | this.player.addClass('videojs-webrtc-plugin'); 63 | }); 64 | this.player.on('playing', () => { 65 | if (this.player.el().getElementsByClassName('vjs-custom-spinner').length) { 66 | this.player.el().removeChild(this.player.spinner); 67 | } 68 | }); 69 | 70 | videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton); 71 | videojs.registerComponent('ResolutionMenuItem', ResolutionMenuItem); 72 | } 73 | /** 74 | * Initiate WebRTCAdaptor. 75 | * 76 | * @param {Object} [options] 77 | * An optional options object. 78 | * 79 | */ 80 | initiateWebRTCAdaptor(source, options) { 81 | 82 | this.options = videojs.mergeOptions(defaults, options); 83 | this.source = source; 84 | 85 | if (typeof source.iceServers === 'object') { 86 | this.source.pcConfig = { iceServers: source.iceServers }; 87 | } else if (typeof source.iceServers === 'string') { 88 | this.source.pcConfig = { iceServers: JSON.parse(source.iceServers) }; 89 | } 90 | 91 | // replace the stream name with websocket url 92 | this.source.mediaServerUrl = source.src.replace(source.src.split('/').at(-1), 'websocket'); 93 | // get the stream name from the url 94 | this.source.streamName = source.src.split('/').at(-1).split('.webrtc')[0]; 95 | 96 | this.source.token = this.getUrlParameter('token'); 97 | this.source.subscriberId = this.getUrlParameter('subscriberId'); 98 | this.source.subscriberCode = this.getUrlParameter('subscriberCode'); 99 | this.source.reconnect = this.source.reconnect === undefined ? true : this.source.reconnect; 100 | 101 | const config = { 102 | websocketURL: this.source.mediaServerUrl, 103 | mediaConstraints: this.source.mediaConstraints, 104 | 105 | isPlayMode: true, 106 | sdpConstraints: this.source.sdpConstraints, 107 | reconnectIfRequiredFlag: this.source.reconnect, 108 | 109 | callback: (info, obj) => { 110 | if (this.disposed) { 111 | return; 112 | } 113 | this.player.trigger('webrtc-info', { obj, info }); 114 | switch (info) { 115 | case ANT_CALLBACKS.INITIALIZED: { 116 | this.play(); 117 | break; 118 | } 119 | case ANT_CALLBACKS.ICE_CONNECTION_STATE_CHANGED: { 120 | 121 | break; 122 | } 123 | case ANT_CALLBACKS.PLAY_STARTED: { 124 | this.joinStreamHandler(obj); 125 | this.isPlaying = true; 126 | this.player.trigger('play'); 127 | break; 128 | } 129 | case ANT_CALLBACKS.PLAY_FINISHED: { 130 | this.leaveStreamHandler(obj); 131 | this.isPlaying = false; 132 | this.player.trigger('ended'); 133 | break; 134 | } 135 | case ANT_CALLBACKS.STREAM_INFORMATION: { 136 | this.streamInformationHandler(obj); 137 | break; 138 | } 139 | case ANT_CALLBACKS.RESOLUTION_CHANGE_INFO: { 140 | this.resolutionChangeHandler(obj); 141 | break; 142 | } 143 | case ANT_CALLBACKS.DATA_RECEIVED: { 144 | this.player.trigger('webrtc-data-received', { obj }); 145 | break; 146 | } 147 | case ANT_CALLBACKS.DATACHANNEL_NOT_OPEN: { 148 | break; 149 | } 150 | case ANT_CALLBACKS.NEW_TRACK_AVAILABLE: { 151 | const vid = this.player.tech().el(); 152 | 153 | if (vid.srcObject !== obj.stream) { 154 | vid.srcObject = obj.stream; 155 | } 156 | break; 157 | } 158 | } 159 | }, 160 | callbackError: (error, message) => { 161 | 162 | if (this.disposed) { 163 | return; 164 | } 165 | // some of the possible errors, NotFoundError, SecurityError,PermissionDeniedError 166 | const ModalDialog = videojs.getComponent('ModalDialog'); 167 | 168 | if (this.errorModal) { 169 | this.errorModal.close(); 170 | } 171 | this.errorModal = new ModalDialog(this.player, { 172 | content: `ERROR: ${JSON.stringify(error)}`, 173 | temporary: true, 174 | pauseOnOpen: false, 175 | uncloseable: true 176 | }); 177 | this.player.addChild(this.errorModal); 178 | this.errorModal.open(); 179 | this.errorModal.setTimeout(() => this.errorModal.close(), 3000); 180 | this.player.trigger('webrtc-error', { error, message }); 181 | 182 | } 183 | }; 184 | 185 | if (this.source.pcConfig) { 186 | /* eslint-disable camelcase */ 187 | const peerconnection_config = {}; 188 | 189 | Object.assign(peerconnection_config, this.source.pcConfig); 190 | 191 | Object.assign(config, { peerconnection_config }); 192 | } 193 | this.webRTCAdaptor = new WebRTCAdaptor(config); 194 | } 195 | 196 | /** 197 | * after websocket success connection. 198 | */ 199 | play() { 200 | this.webRTCAdaptor.play( 201 | this.source.streamName, 202 | this.source.token, 203 | null, 204 | null, 205 | this.source.subscriberId, 206 | this.source.subscriberCode, 207 | null 208 | ); 209 | 210 | } 211 | /** 212 | * after joined stream handler 213 | * 214 | * @param {Object} obj callback artefacts 215 | */ 216 | joinStreamHandler(obj) { 217 | this.webRTCAdaptor.getStreamInfo(this.source.streamName); 218 | } 219 | /** 220 | * after left stream. 221 | */ 222 | leaveStreamHandler() { 223 | // reset stream resolutions in dropdown 224 | this.player.resolutions = []; 225 | // eslint-disable-next-line newline-after-var 226 | const resolutionButton = this.player.controlBar.getChild('ResolutionMenuButton'); 227 | if (resolutionButton) { 228 | resolutionButton.update(); 229 | } 230 | } 231 | /** 232 | * stream information handler. 233 | * 234 | * @param {Object} obj callback artefacts 235 | */ 236 | streamInformationHandler(obj) { 237 | const streamResolutions = obj.streamInfo.reduce((unique, item) => 238 | unique.includes(item.streamHeight) ? unique : [...unique, item.streamHeight], []).sort((a, b) => b - a); 239 | 240 | this.player.resolutions = streamResolutions.map((resolution) => ({ 241 | label: resolution, 242 | value: resolution 243 | })); 244 | this.player.selectedResolution = 0; 245 | this.addResolutionButton(); 246 | } 247 | addResolutionButton() { 248 | const controlBar = this.player.controlBar; 249 | const fullscreenToggle = controlBar.getChild('fullscreenToggle').el(); 250 | 251 | if (controlBar.getChild('ResolutionMenuButton')) { 252 | controlBar.removeChild('ResolutionMenuButton'); 253 | } 254 | 255 | controlBar.el().insertBefore(controlBar.addChild('ResolutionMenuButton', { 256 | plugin: this, 257 | streamName: this.source.streamName 258 | }).el(), fullscreenToggle); 259 | } 260 | /** 261 | * change resolution handler. 262 | * 263 | * @param {Object} obj callback artefacts 264 | */ 265 | resolutionChangeHandler(obj) { 266 | // eslint-disable-next-line no-undef 267 | this.player.spinner = document.createElement('div'); 268 | 269 | this.player.spinner.className = 'vjs-custom-spinner'; 270 | this.player.el().appendChild(this.player.spinner); 271 | this.player.pause(); 272 | this.player.setTimeout(() => { 273 | if (this.player.el().getElementsByClassName('vjs-custom-spinner').length) { 274 | this.player.el().removeChild(this.player.spinner); 275 | this.player.play(); 276 | } 277 | }, 2000); 278 | } 279 | changeStreamQuality(value) { 280 | this.webRTCAdaptor.forceStreamQuality(this.source.streamName, value); 281 | this.player.selectedResolution = value; 282 | this.player.controlBar.getChild('ResolutionMenuButton').update(); 283 | 284 | } 285 | 286 | /** 287 | * get url parameter 288 | * 289 | * @param {string} param callback event info 290 | */ 291 | getUrlParameter(param) { 292 | if (this.source.src.includes('?')) { 293 | const urlParams = this.source.src.split('?')[1].split('&').reduce( 294 | (p, e) => { 295 | const a = e.split('='); 296 | 297 | p[decodeURIComponent(a[0])] = decodeURIComponent(a[1]); 298 | return p; 299 | }, 300 | {} 301 | ) || {}; 302 | 303 | return urlParams[param]; 304 | } 305 | return null; 306 | } 307 | 308 | dispose() { 309 | this.disposed = true; 310 | if (this.webRTCAdaptor) { 311 | this.webRTCAdaptor.stop(this.source.streamName); 312 | this.webRTCAdaptor.closeWebSocket(); 313 | this.webRTCAdaptor = null; 314 | } 315 | } 316 | } 317 | 318 | const webRTCSourceHandler = { 319 | name: 'videojs-webrtc-plugin', 320 | VERSION: '1.3.1', 321 | 322 | canHandleSource(srcObj, options = {}) { 323 | const localOptions = videojs.mergeOptions(videojs.options, options); 324 | 325 | localOptions.source = srcObj.src; 326 | 327 | return webRTCSourceHandler.canPlayType(srcObj.type, localOptions); 328 | }, 329 | handleSource(source, tech, options = {}) { 330 | const localOptions = videojs.mergeOptions(videojs.options, options); 331 | 332 | // setting the src already dispose the component, no need to dispose it again 333 | tech.webrtc = new WebRTCHandler(source, tech, localOptions); 334 | 335 | return tech.webrtc; 336 | }, 337 | 338 | canPlayType(type, options = {}) { 339 | 340 | const mediaUrl = options.source; 341 | const regex = /\.webrtc.*$/; 342 | const isMatch = regex.test(mediaUrl); 343 | 344 | if (isMatch) { 345 | return 'maybe'; 346 | } 347 | 348 | return ''; 349 | } 350 | }; 351 | 352 | // register source handlers with the appropriate techs 353 | videojs.getTech('Html5').registerSourceHandler(webRTCSourceHandler, 0); 354 | 355 | export default 356 | { 357 | WebRTCHandler, 358 | webRTCSourceHandler 359 | }; 360 | -------------------------------------------------------------------------------- /src/plugin.scss: -------------------------------------------------------------------------------- 1 | .vjs-custom-spinner { 2 | position: absolute; 3 | top: 50%; 4 | transform: translateY(-50%) translateX(-50%); 5 | left: 50%; 6 | width: 80px; 7 | height: 80px; 8 | } 9 | .vjs-custom-spinner:after { 10 | content: " "; 11 | display: block; 12 | width: 64px; 13 | height: 64px; 14 | margin: 8px; 15 | border-radius: 50%; 16 | border: 6px solid #fff; 17 | border-color: #fff transparent #fff transparent; 18 | animation: vjs-spinner-spin 1.1s cubic-bezier(0.6, 0.2, 0, 0.8) infinite, vjs-spinner-fade 1.1s linear infinite; 19 | } 20 | -------------------------------------------------------------------------------- /test/plugin.test.js: -------------------------------------------------------------------------------- 1 | import document from 'global/document'; 2 | 3 | import QUnit from 'qunit'; 4 | import sinon from 'sinon'; 5 | import videojs from 'video.js'; 6 | 7 | import plugin from '../src/plugin.js'; 8 | import { ANT_CALLBACKS } from '../src/const/CALLBACKS.js'; 9 | 10 | const STATIC_VIDEO_HTML = ""; 11 | 12 | QUnit.test('the environment is sane', function(assert) { 13 | assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists'); 14 | assert.strictEqual(typeof sinon, 'object', 'sinon exists'); 15 | assert.strictEqual(typeof videojs, 'function', 'videojs exists'); 16 | assert.strictEqual(typeof plugin, 'object', 'plugin is a object'); 17 | }); 18 | QUnit.test('play finished before play started', function(assert) { 19 | 20 | const videoContainer = document.createElement('video_container'); 21 | 22 | videoContainer.innerHTML = STATIC_VIDEO_HTML; 23 | 24 | document.getElementById('qunit-fixture').appendChild(videoContainer); 25 | 26 | const iceServer = '[ { "urls": "turn:ovh36.antmedia.io" } ]'; 27 | 28 | const webrtcHandler = new plugin.WebRTCHandler( 29 | { 30 | src: 'ws://localhost:5080/WebRTCAppEE/stream.webrtc', 31 | type: 'video/webrtc', 32 | withCredentials: true, 33 | iceServers: iceServer, 34 | reconnect: false 35 | }, 36 | null, 37 | { 38 | playerId: 'video-player' 39 | } 40 | ); 41 | 42 | assert.ok(webrtcHandler.webRTCAdaptor); 43 | 44 | assert.ok(webrtcHandler.webRTCAdaptor.callback); 45 | // this callback triggers an error and this code does not pass 46 | // https://github.com/ant-media/Ant-Media-Server/issues/6990 47 | webrtcHandler.webRTCAdaptor.callback(ANT_CALLBACKS.PLAY_FINISHED); 48 | }); 49 | 50 | QUnit.test('PeerConnection Config', function(assert) { 51 | 52 | const videoContainer = document.createElement('video_container'); 53 | 54 | videoContainer.innerHTML = STATIC_VIDEO_HTML; 55 | 56 | document.getElementById('qunit-fixture').appendChild(videoContainer); 57 | 58 | const element = document.getElementById('video-player'); 59 | 60 | assert.ok(element, 'Video Element is created'); 61 | 62 | { 63 | const iceServer = '[ { "urls": "turn:ovh36.antmedia.io" } ]'; 64 | 65 | const webrtcHandler = new plugin.WebRTCHandler( 66 | { 67 | src: 'ws://localhost:5080/WebRTCAppEE/stream.webrtc', 68 | type: 'video/webrtc', 69 | withCredentials: true, 70 | iceServers: iceServer, 71 | reconnect: false 72 | }, 73 | null, 74 | { 75 | playerId: 'video-player' 76 | } 77 | ); 78 | 79 | assert.deepEqual(webrtcHandler.webRTCAdaptor.peerconnection_config.iceServers, JSON.parse(iceServer), 'PeerConnection Config is correct'); 80 | } 81 | 82 | { 83 | const iceServer2 = [ 84 | { urls: 'turn:ovh36.antmedia.io' } 85 | ]; 86 | const webrtcHandler2 = new plugin.WebRTCHandler( 87 | { 88 | src: 'ws://localhost:5080/WebRTCAppEE/stream.webrtc', 89 | type: 'video/webrtc', 90 | withCredentials: true, 91 | iceServers: [ 92 | { urls: 'turn:ovh36.antmedia.io' } 93 | ], 94 | reconnect: false 95 | }, 96 | null, 97 | { 98 | playerId: 'video-player' 99 | } 100 | ); 101 | 102 | assert.deepEqual(webrtcHandler2.webRTCAdaptor.peerconnection_config.iceServers, iceServer2, 'PeerConnection Config is correct'); 103 | } 104 | 105 | { 106 | 107 | // default iceServers 108 | const iceServer = [{ 109 | urls: 'stun:stun1.l.google.com:19302' 110 | }]; 111 | 112 | const webrtcHandler = new plugin.WebRTCHandler( 113 | { 114 | src: 'ws://localhost:5080/WebRTCAppEE/stream.webrtc', 115 | type: 'video/webrtc', 116 | withCredentials: true, 117 | reconnect: false 118 | }, 119 | null, 120 | { 121 | playerId: 'video-player' 122 | } 123 | ); 124 | 125 | assert.deepEqual(webrtcHandler.webRTCAdaptor.peerconnection_config.iceServers, iceServer, 'PeerConnection Config is correct'); 126 | } 127 | 128 | }); 129 | 130 | QUnit.module('videojs-webrtc-plugin', { 131 | 132 | beforeEach() { 133 | 134 | // Mock the environment's timers because certain things - particularly 135 | // player readiness - are asynchronous in video.js 5. This MUST come 136 | // before any player is created; otherwise, timers could get created 137 | // with the actual timer methods! 138 | this.clock = sinon.useFakeTimers(); 139 | 140 | this.fixture = document.getElementById('qunit-fixture'); 141 | this.video = document.createElement('video'); 142 | this.fixture.appendChild(this.video); 143 | this.player = videojs(this.video); 144 | }, 145 | 146 | afterEach() { 147 | this.player.dispose(); 148 | this.clock.restore(); 149 | } 150 | }); 151 | 152 | --------------------------------------------------------------------------------