├── .eslintrc ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── e2e-tests.yaml │ └── lint-and-unit-tests.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── bower.json ├── index.d.ts ├── package.json ├── release ├── adapter.js ├── adapter_no_edge.js ├── adapter_no_edge_no_global.js ├── adapter_no_global.js └── esbuild.js ├── src └── js │ ├── adapter_core.js │ ├── adapter_core5.js │ ├── adapter_factory.js │ ├── chrome │ ├── chrome_shim.js │ └── getusermedia.js │ ├── common_shim.js │ ├── firefox │ ├── firefox_shim.js │ ├── getdisplaymedia.js │ └── getusermedia.js │ ├── safari │ └── safari_shim.js │ └── utils.js └── test ├── .eslintrc ├── README.md ├── e2e ├── .eslintrc ├── addIceCandidate.js ├── addTrack.js ├── browserdetails.js ├── connection.js ├── dtmf.js ├── expectations │ ├── MicrosoftEdge │ ├── chrome-beta │ ├── chrome-beta-no-experimental │ ├── chrome-dev │ ├── chrome-stable │ ├── chrome-stable-no-experimental │ ├── firefox-beta │ ├── firefox-esr │ ├── firefox-nightly │ └── firefox-stable ├── getStats.js ├── getusermedia.js ├── maxMessageSize.js ├── mediaDevices.js ├── mediastream.js ├── msid.js ├── negotiationneeded.js ├── ontrack.js ├── removeTrack.js ├── rtcicecandidate.js ├── rtcpeerconnection.js ├── rtcsessiondescription.js ├── simulcast.js └── srcobject.js ├── getusermedia-mocha.js ├── karma.conf.js ├── testpage.html └── unit ├── .eslintrc ├── adapterfactory.test.js ├── addicecandidate.test.js ├── chrome.test.js ├── compactObject.test.js ├── detectBrowser.test.js ├── extmap-allow-mixed.test.js ├── extractVersion.test.js ├── firefox.test.js ├── logSuppression.test.js ├── rtcicecandidate.test.js └── safari.test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "array-bracket-spacing": 2, 4 | "block-spacing": [2, "never"], 5 | "brace-style": [2, "1tbs", {"allowSingleLine": false}], 6 | "camelcase": [2, {"properties": "always"}], 7 | "curly": 2, 8 | "default-case": 2, 9 | "dot-notation": 2, 10 | "eqeqeq": 2, 11 | "id-match": ["error", "^[\x00-\x7F]+$", { 12 | "properties": true, 13 | "onlyDeclarations": false, 14 | "ignoreDestructuring": false 15 | }], 16 | "indent": [ 17 | 2, 18 | 2, 19 | {"SwitchCase": 1} 20 | ], 21 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 22 | "keyword-spacing": 2, 23 | "max-len": [2, 80, 2, {"ignoreUrls": true}], 24 | "new-cap": [2, {"newIsCapExceptions": [ 25 | "webkitRTCPeerConnection", 26 | "mozRTCPeerConnection" 27 | ]}], 28 | "no-console": 0, 29 | "no-else-return": 2, 30 | "no-eval": 2, 31 | "no-multi-spaces": 2, 32 | "no-multiple-empty-lines": [2, {"max": 2}], 33 | "no-shadow": 2, 34 | "no-trailing-spaces": 2, 35 | "no-unused-expressions": 2, 36 | "no-unused-vars": [2, {"args": "none"}], 37 | "object-curly-spacing": [2, "never"], 38 | "padded-blocks": [2, "never"], 39 | "quotes": [ 40 | 2, 41 | "single" 42 | ], 43 | "semi": [ 44 | 2, 45 | "always" 46 | ], 47 | "space-before-blocks": 2, 48 | "space-before-function-paren": [2, "never"], 49 | "space-unary-ops": 2, 50 | "space-infix-ops": 2, 51 | "spaced-comment": 2, 52 | "valid-typeof": 2 53 | }, 54 | "env": { 55 | "browser": true, 56 | "es6": true, 57 | "node": true 58 | }, 59 | "extends": ["eslint:recommended"], 60 | "parserOptions": { 61 | "sourceType": "module" 62 | }, 63 | "globals": { 64 | "module": true, 65 | "require": true, 66 | "process": true, 67 | "Promise": true, 68 | "Map": true 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Please read first! 2 | Please use [discuss-webrtc](https://groups.google.com/forum/#!forum/discuss-webrtc) for general technical discussions and questions. 3 | 4 | - [ ] I have provided steps to reproduce (e.g. a link to a [jsfiddle](https://jsfiddle.net/)) 5 | - [ ] I have provided browser name, version and adapter.js version 6 | - [ ] This issue only happens when adapter.js is used 7 | 8 | **Note: If the checkboxes above are not checked (which you do after the issue is posted), the issue will be closed.** 9 | 10 | ## Versions affected 11 | 12 | **Browser name including version (e.g. Chrome 64.0.3282.119)** 13 | 14 | 15 | **adapter.js (e.g. 6.1.0)** 16 | 17 | 18 | ## Description 19 | 20 | 21 | ## Steps to reproduce 22 | 23 | 24 | ## Expected results 25 | 26 | 27 | ## Actual results 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Description** 2 | 3 | 4 | **Purpose** 5 | -------------------------------------------------------------------------------- /.github/workflows/e2e-tests.yaml: -------------------------------------------------------------------------------- 1 | name: e2e-tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | browser: [chrome, firefox] 11 | version: [stable, beta] 12 | include: 13 | - browser: chrome 14 | version: dev 15 | - browser: firefox 16 | version: nightly 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | - run: npm install 21 | - run: Xvfb :99 & 22 | - name: e2e-tests 23 | env: 24 | BROWSER: ${{matrix.browser}} 25 | BVER: ${{matrix.version}} 26 | DISPLAY: :99.0 27 | run: npm run e2e-tests 28 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: lint-and-unit-tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v4 11 | - run: npm install 12 | - run: npm run lint-and-unit-tests 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | browsers/ 2 | firefox-*.tar.bz2 3 | .DS_Store 4 | node_modules/ 5 | out/ 6 | dist/ 7 | validation-report.json 8 | validation-status.json 9 | npm-debug.log 10 | *~ 11 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | browsers/ 2 | browser-tmp/ 3 | firefox-*.tar.bz2 4 | .DS_Store 5 | node_modules/ 6 | validation-report.json 7 | validation-status.json 8 | npm-debug.log 9 | *~ 10 | release/ 11 | test/ 12 | .github/ 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | WebRTC welcomes patches/pulls for features and bug fixes. 2 | 3 | For contributors external to Google, follow the instructions given in the [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual). 4 | 5 | In all cases, contributors must sign a contributor license agreement before a contribution can be accepted. Please complete the agreement for an [individual](https://developers.google.com/open-source/cla/individual) or a [corporation](https://developers.google.com/open-source/cla/corporate) as appropriate. 6 | 7 | If you plan to add a significant component or large chunk of code, we recommend you bring this up on the [webrtc-discuss group](https://groups.google.com/forum/#!forum/discuss-webrtc) for a design discussion before writing code. 8 | 9 | If appropriate, write a unit test which demonstrates that your code functions as expected. Tests are the best way to ensure that future contributors do not break your code accidentally. 10 | 11 | To request a change or addition, you must [submit a pull request](https://help.github.com/categories/collaborating/). 12 | 13 | WebRTC developers monitor outstanding pull requests. They may request changes to the pull request before accepting. They will also verify that a CLA has been signed. 14 | 15 | The [Developer's Guide](https://bit.ly/webrtcdevguide) for this repo has more detailed information about code style, structure and validation. 16 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | babel: { 7 | options: { 8 | presets: ['@babel/preset-env'] 9 | }, 10 | dist: { 11 | files: [{ 12 | expand: 'true', 13 | cwd: 'src/js', 14 | src: ['*.js', '**/*.js'], 15 | dest: 'dist/' 16 | }] 17 | } 18 | }, 19 | browserify: { 20 | adapterGlobalObject: { 21 | src: ['./dist/adapter_core5.js'], 22 | dest: './out/adapter.js', 23 | options: { 24 | browserifyOptions: { 25 | // Exposes shim methods in a global object to the browser. 26 | // The tests require this. 27 | standalone: 'adapter' 28 | } 29 | } 30 | }, 31 | // Use this if you do not want adapter to expose anything to the global 32 | // scope. 33 | adapterAndNoGlobalObject: { 34 | src: ['./dist/adapter_core5.js'], 35 | dest: './out/adapter_no_global.js' 36 | } 37 | }, 38 | eslint: { 39 | options: { 40 | overrideConfigFile: '.eslintrc' 41 | }, 42 | target: ['src/**/*.js', 'test/*.js', 'test/unit/*.js', 'test/e2e/*.js'] 43 | }, 44 | copy: { 45 | build: { 46 | dest: 'release/', 47 | cwd: 'out', 48 | src: '**', 49 | nonull: true, 50 | expand: true 51 | } 52 | }, 53 | }); 54 | 55 | grunt.loadNpmTasks('grunt-eslint'); 56 | grunt.loadNpmTasks('grunt-browserify'); 57 | grunt.loadNpmTasks('grunt-babel'); 58 | grunt.loadNpmTasks('grunt-contrib-copy'); 59 | 60 | grunt.registerTask('default', ['eslint', 'build']); 61 | grunt.registerTask('lint', ['eslint']); 62 | grunt.registerTask('build', ['babel', 'browserify']); 63 | grunt.registerTask('copyForPublish', ['copy']); 64 | grunt.registerTask('downloadBrowser', ['shell:downloadBrowser']) 65 | }; 66 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, The WebRTC project authors. All rights reserved. 2 | Copyright (c) 2018, The adapter.js project authors. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in 13 | the documentation and/or other materials provided with the 14 | distribution. 15 | 16 | * Neither the name of Google nor the names of its contributors may 17 | be used to endorse or promote products derived from this software 18 | without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebRTC adapter # 2 | adapter.js is a shim to insulate apps from spec changes and prefix differences in WebRTC. The prefix differences are mostly gone these days but differences in behaviour between browsers remain. 3 | 4 | This repository used to be part of the WebRTC organisation on github but moved. We aim to keep the old repository updated with new releases. 5 | 6 | ## Install ## 7 | 8 | #### NPM 9 | ```bash 10 | npm install webrtc-adapter 11 | ``` 12 | 13 | #### Bower 14 | ```bash 15 | bower install webrtc-adapter 16 | ``` 17 | 18 | ## Usage ## 19 | ##### Javascript 20 | Just import adapter: 21 | ``` 22 | import adapter from 'webrtc-adapter'; 23 | ``` 24 | No further action is required. You might want to use adapters browser detection 25 | which detects which webrtc quirks are required. You can look at 26 | ``` 27 | adapter.browserDetails.browser 28 | ``` 29 | for webrtc engine detection (which will for example detect Opera or the Chromium based Edge as 'chrome') and 30 | ``` 31 | adapter.browserDetails.version 32 | ``` 33 | for the version according to the user-agent string. 34 | 35 | ##### NPM 36 | Copy to desired location in your src tree or use a minify/vulcanize tool (node_modules is usually not published with the code). 37 | See [webrtc/samples repo](https://github.com/webrtc/samples) as an example on how you can do this. 38 | 39 | #### Prebuilt releases 40 | ##### Web 41 | In the [gh-pages branch](https://github.com/webrtcHacks/adapter/tree/gh-pages) prebuilt ready to use files can be downloaded/linked directly. 42 | Latest version can be found at https://webrtc.github.io/adapter/adapter-latest.js. 43 | Specific versions can be found at https://webrtc.github.io/adapter/adapter-N.N.N.js, e.g. https://webrtc.github.io/adapter/adapter-1.0.2.js. 44 | 45 | ##### Bower 46 | You will find `adapter.js` in `bower_components/webrtc-adapter/`. 47 | 48 | ##### NPM 49 | In node_modules/webrtc-adapter/out/ folder you will find 4 files: 50 | * `adapter.js` - includes all the shims and is visible in the browser under the global `adapter` object (window.adapter). 51 | * `adapter_no_global.js` - same as `adapter.js` but is not exposed/visible in the browser (you cannot call/interact with the shims in the browser). 52 | 53 | Include the file that suits your need in your project. 54 | 55 | ## Development ## 56 | Head over to [test/README.md](https://github.com/webrtcHacks/adapter/blob/master/test/README.md) and get started developing. 57 | 58 | ## Publish a new version ## 59 | * Go to the adapter repository root directory 60 | * Make sure your repository is clean, i.e. no untracked files etc. Also check that you are on the master branch and have pulled the latest changes. 61 | * Depending on the impact of the release, either use `patch`, `minor` or `major` in place of ``. Run `npm version -m 'bump to %s'` and type in your password lots of times (setting up credential caching is probably a good idea). 62 | * Create and merge the PR if green in the GitHub web ui 63 | * Go to the releases tab in the GitHub web ui and edit the tag. 64 | * Add a summary of the recent commits in the tag summary and a link to the diff between the previous and current version in the description, [example](https://github.com/webrtcHacks/adapter/releases/tag/v3.4.1). 65 | * Go back to your checkout and run `git pull` 66 | * Run `npm publish` (you need access to the [webrtc-adapter npmjs package](https://www.npmjs.com/package/webrtc-adapter)). For big changes, consider using a [tag version](https://docs.npmjs.com/adding-dist-tags-to-packages) such as `next` and then [change the dist-tag after testing](https://docs.npmjs.com/cli/dist-tag). 67 | * Done! There should now be a new release published to NPM and the gh-pages branch. 68 | 69 | Note: Currently only tested on Linux, not sure about Mac but will definitely not work on Windows. 70 | 71 | ### Publish a hotfix patch versions 72 | In some cases it may be necessary to do a patch version while there are significant changes changes on the master branch. 73 | To make a patch release, 74 | * checkout the latest git tag using `git checkout tags/vMajor.minor.patch`. 75 | * checkout a new branch, using a name such as patchrelease-major-minor-patch. 76 | * cherry-pick the fixes using `git cherry-pick some-commit-hash`. 77 | * run `npm version patch`. This will create a new patch version and publish it on github. 78 | * check out `origin/bumpVersion` branch and publish the new version using `npm publish`. 79 | * the branch can now safely be deleted. It is not necessary to merge it into the main branch since it only contains cherry-picked commits. 80 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtc-adapter", 3 | "description": "A shim to insulate apps from WebRTC spec changes and browser prefix differences", 4 | "license": "BSD-3-Clause", 5 | "main": "./release/adapter.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/webrtchacks/adapter.git" 9 | }, 10 | "authors": [ 11 | "The WebRTC project authors (https://www.webrtc.org/)", 12 | "The adapter.js project authors (https://github.com/webrtchacks/adapter/)" 13 | ], 14 | "moduleType": [ 15 | "node" 16 | ], 17 | "ignore": [ 18 | "test/*" 19 | ], 20 | "keywords": [ 21 | "WebRTC", 22 | "RTCPeerConnection", 23 | "getUserMedia" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "webrtc-adapter" { 2 | interface IBrowserDetails { 3 | browser: string; 4 | version?: number; 5 | supportsUnifiedPlan?: boolean; 6 | } 7 | 8 | interface ICommonShim { 9 | shimRTCIceCandidate(window: Window): void; 10 | shimMaxMessageSize(window: Window): void; 11 | shimSendThrowTypeError(window: Window): void; 12 | shimConnectionState(window: Window): void; 13 | removeAllowExtmapMixed(window: Window): void; 14 | } 15 | 16 | interface IChromeShim { 17 | shimMediaStream(window: Window): void; 18 | shimOnTrack(window: Window): void; 19 | shimGetSendersWithDtmf(window: Window): void; 20 | shimSenderReceiverGetStats(window: Window): void; 21 | shimAddTrackRemoveTrackWithNative(window: Window): void; 22 | shimAddTrackRemoveTrack(window: Window): void; 23 | shimPeerConnection(window: Window): void; 24 | fixNegotiationNeeded(window: Window): void; 25 | } 26 | 27 | interface IFirefoxShim { 28 | shimOnTrack(window: Window): void; 29 | shimPeerConnection(window: Window): void; 30 | shimSenderGetStats(window: Window): void; 31 | shimReceiverGetStats(window: Window): void; 32 | shimRemoveStream(window: Window): void; 33 | shimRTCDataChannel(window: Window): void; 34 | } 35 | 36 | interface ISafariShim { 37 | shimLocalStreamsAPI(window: Window): void; 38 | shimRemoteStreamsAPI(window: Window): void; 39 | shimCallbacksAPI(window: Window): void; 40 | shimGetUserMedia(window: Window): void; 41 | shimConstraints(constraints: MediaStreamConstraints): void; 42 | shimRTCIceServerUrls(window: Window): void; 43 | shimTrackEventTransceiver(window: Window): void; 44 | shimCreateOfferLegacy(window: Window): void; 45 | } 46 | 47 | export interface IAdapter { 48 | browserDetails: IBrowserDetails; 49 | commonShim: ICommonShim; 50 | browserShim: IChromeShim | IFirefoxShim | ISafariShim | undefined; 51 | extractVersion(uastring: string, expr: string, pos: number): number; 52 | disableLog(disable: boolean): void; 53 | disableWarnings(disable: boolean): void; 54 | } 55 | 56 | const adapter: IAdapter; 57 | export default adapter; 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtc-adapter", 3 | "version": "9.0.1", 4 | "description": "A shim to insulate apps from WebRTC spec changes and browser prefix differences", 5 | "license": "BSD-3-Clause", 6 | "main": "./dist/adapter_core.js", 7 | "types": "./index.d.ts", 8 | "module": "./src/js/adapter_core.js", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/webrtchacks/adapter.git" 12 | }, 13 | "authors": [ 14 | "The WebRTC project authors (https://www.webrtc.org/)", 15 | "The adapter.js project authors (https://github.com/webrtchacks/adapter/)" 16 | ], 17 | "scripts": { 18 | "preversion": "git stash && npm install && npm update && BROWSER=chrome BVER=stable CI=true npm test && git checkout -B bumpVersion && grunt build && grunt copyForPublish && git add package.json release/* && git commit -m 'Add adapter artifacts' --allow-empty", 19 | "version": "", 20 | "postversion": "export GITTAG=\"echo $(git describe --abbrev=0 --tags | sed 's/^v//')\" && git push --force --set-upstream origin bumpVersion --follow-tags && git checkout gh-pages && git pull && cp out/adapter.js adapter.js && cp adapter.js adapter-`$GITTAG`.js && rm adapter-latest.js && ln -s adapter-`$GITTAG`.js adapter-latest.js && mkdir -p adapter-`$GITTAG`-variants && cp out/adapter.js adapter-`$GITTAG`-variants/ && cp out/adapter_*.js adapter-`$GITTAG`-variants/ && git add adapter.js adapter-latest.js adapter-`$GITTAG`.js adapter-`$GITTAG`-variants && git commit -m `$GITTAG` && git push --set-upstream origin gh-pages && git checkout main", 21 | "prepare": "grunt build", 22 | "prepublishonly": "npm test", 23 | "test": "grunt && jest test/unit && karma start test/karma.conf.js", 24 | "lint-and-unit-tests": "grunt && jest test/unit", 25 | "e2e-tests": "grunt && karma start test/karma.conf.js" 26 | }, 27 | "dependencies": { 28 | "sdp": "^3.2.0" 29 | }, 30 | "engines": { 31 | "npm": ">=3.10.0", 32 | "node": ">=6.0.0" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.21.0", 36 | "@babel/preset-env": "^7.20.2", 37 | "@puppeteer/browsers": "^2.2.0", 38 | "babel-preset-env": "^1.7.0", 39 | "brfs": "^1.5.0", 40 | "chai": "^3.5.0", 41 | "eslint-plugin-jest": "^27.4.0", 42 | "grunt": "^1.1.0", 43 | "grunt-babel": "^8.0.0", 44 | "grunt-browserify": "^6.0.0", 45 | "grunt-cli": "^1.3.1", 46 | "grunt-contrib-clean": "^1.1.0", 47 | "grunt-contrib-copy": "^1.0.0", 48 | "grunt-eslint": "^24.0.0", 49 | "jest": "^29.7.0", 50 | "karma": "^6.4.1", 51 | "karma-browserify": "^8.1.0", 52 | "karma-chai": "^0.1.0", 53 | "karma-chrome-launcher": "^2.2.0", 54 | "karma-firefox-launcher": "^1.3.0", 55 | "karma-mocha": "^2.0.1", 56 | "karma-mocha-reporter": "^2.2.3", 57 | "karma-safari-launcher": "^1.0.0", 58 | "karma-stability-reporter": "^3.0.1", 59 | "mocha": "^10.2.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/js/adapter_core.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 10 | 'use strict'; 11 | 12 | import {adapterFactory} from './adapter_factory.js'; 13 | 14 | const adapter = 15 | adapterFactory({window: typeof window === 'undefined' ? undefined : window}); 16 | export default adapter; 17 | -------------------------------------------------------------------------------- /src/js/adapter_core5.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 10 | 'use strict'; 11 | 12 | import {adapterFactory} from './adapter_factory.js'; 13 | 14 | const adapter = 15 | adapterFactory({window: typeof window === 'undefined' ? undefined : window}); 16 | module.exports = adapter; // this is the difference from adapter_core. 17 | -------------------------------------------------------------------------------- /src/js/adapter_factory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | import * as utils from './utils'; 9 | 10 | // Browser shims. 11 | import * as chromeShim from './chrome/chrome_shim'; 12 | import * as firefoxShim from './firefox/firefox_shim'; 13 | import * as safariShim from './safari/safari_shim'; 14 | import * as commonShim from './common_shim'; 15 | import * as sdp from 'sdp'; 16 | 17 | // Shimming starts here. 18 | export function adapterFactory({window} = {}, options = { 19 | shimChrome: true, 20 | shimFirefox: true, 21 | shimSafari: true, 22 | }) { 23 | // Utils. 24 | const logging = utils.log; 25 | const browserDetails = utils.detectBrowser(window); 26 | 27 | const adapter = { 28 | browserDetails, 29 | commonShim, 30 | extractVersion: utils.extractVersion, 31 | disableLog: utils.disableLog, 32 | disableWarnings: utils.disableWarnings, 33 | // Expose sdp as a convenience. For production apps include directly. 34 | sdp, 35 | }; 36 | 37 | // Shim browser if found. 38 | switch (browserDetails.browser) { 39 | case 'chrome': 40 | if (!chromeShim || !chromeShim.shimPeerConnection || 41 | !options.shimChrome) { 42 | logging('Chrome shim is not included in this adapter release.'); 43 | return adapter; 44 | } 45 | if (browserDetails.version === null) { 46 | logging('Chrome shim can not determine version, not shimming.'); 47 | return adapter; 48 | } 49 | logging('adapter.js shimming chrome.'); 50 | // Export to the adapter global object visible in the browser. 51 | adapter.browserShim = chromeShim; 52 | 53 | // Must be called before shimPeerConnection. 54 | commonShim.shimAddIceCandidateNullOrEmpty(window, browserDetails); 55 | commonShim.shimParameterlessSetLocalDescription(window, browserDetails); 56 | 57 | chromeShim.shimGetUserMedia(window, browserDetails); 58 | chromeShim.shimMediaStream(window, browserDetails); 59 | chromeShim.shimPeerConnection(window, browserDetails); 60 | chromeShim.shimOnTrack(window, browserDetails); 61 | chromeShim.shimAddTrackRemoveTrack(window, browserDetails); 62 | chromeShim.shimGetSendersWithDtmf(window, browserDetails); 63 | chromeShim.shimSenderReceiverGetStats(window, browserDetails); 64 | chromeShim.fixNegotiationNeeded(window, browserDetails); 65 | 66 | commonShim.shimRTCIceCandidate(window, browserDetails); 67 | commonShim.shimRTCIceCandidateRelayProtocol(window, browserDetails); 68 | commonShim.shimConnectionState(window, browserDetails); 69 | commonShim.shimMaxMessageSize(window, browserDetails); 70 | commonShim.shimSendThrowTypeError(window, browserDetails); 71 | commonShim.removeExtmapAllowMixed(window, browserDetails); 72 | break; 73 | case 'firefox': 74 | if (!firefoxShim || !firefoxShim.shimPeerConnection || 75 | !options.shimFirefox) { 76 | logging('Firefox shim is not included in this adapter release.'); 77 | return adapter; 78 | } 79 | logging('adapter.js shimming firefox.'); 80 | // Export to the adapter global object visible in the browser. 81 | adapter.browserShim = firefoxShim; 82 | 83 | // Must be called before shimPeerConnection. 84 | commonShim.shimAddIceCandidateNullOrEmpty(window, browserDetails); 85 | commonShim.shimParameterlessSetLocalDescription(window, browserDetails); 86 | 87 | firefoxShim.shimGetUserMedia(window, browserDetails); 88 | firefoxShim.shimPeerConnection(window, browserDetails); 89 | firefoxShim.shimOnTrack(window, browserDetails); 90 | firefoxShim.shimRemoveStream(window, browserDetails); 91 | firefoxShim.shimSenderGetStats(window, browserDetails); 92 | firefoxShim.shimReceiverGetStats(window, browserDetails); 93 | firefoxShim.shimRTCDataChannel(window, browserDetails); 94 | firefoxShim.shimAddTransceiver(window, browserDetails); 95 | firefoxShim.shimGetParameters(window, browserDetails); 96 | firefoxShim.shimCreateOffer(window, browserDetails); 97 | firefoxShim.shimCreateAnswer(window, browserDetails); 98 | 99 | commonShim.shimRTCIceCandidate(window, browserDetails); 100 | commonShim.shimConnectionState(window, browserDetails); 101 | commonShim.shimMaxMessageSize(window, browserDetails); 102 | commonShim.shimSendThrowTypeError(window, browserDetails); 103 | break; 104 | case 'safari': 105 | if (!safariShim || !options.shimSafari) { 106 | logging('Safari shim is not included in this adapter release.'); 107 | return adapter; 108 | } 109 | logging('adapter.js shimming safari.'); 110 | // Export to the adapter global object visible in the browser. 111 | adapter.browserShim = safariShim; 112 | 113 | // Must be called before shimCallbackAPI. 114 | commonShim.shimAddIceCandidateNullOrEmpty(window, browserDetails); 115 | commonShim.shimParameterlessSetLocalDescription(window, browserDetails); 116 | 117 | safariShim.shimRTCIceServerUrls(window, browserDetails); 118 | safariShim.shimCreateOfferLegacy(window, browserDetails); 119 | safariShim.shimCallbacksAPI(window, browserDetails); 120 | safariShim.shimLocalStreamsAPI(window, browserDetails); 121 | safariShim.shimRemoteStreamsAPI(window, browserDetails); 122 | safariShim.shimTrackEventTransceiver(window, browserDetails); 123 | safariShim.shimGetUserMedia(window, browserDetails); 124 | safariShim.shimAudioContext(window, browserDetails); 125 | 126 | commonShim.shimRTCIceCandidate(window, browserDetails); 127 | commonShim.shimRTCIceCandidateRelayProtocol(window, browserDetails); 128 | commonShim.shimMaxMessageSize(window, browserDetails); 129 | commonShim.shimSendThrowTypeError(window, browserDetails); 130 | commonShim.removeExtmapAllowMixed(window, browserDetails); 131 | break; 132 | default: 133 | logging('Unsupported browser!'); 134 | break; 135 | } 136 | 137 | return adapter; 138 | } 139 | -------------------------------------------------------------------------------- /src/js/chrome/chrome_shim.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | import * as utils from '../utils.js'; 11 | 12 | export {shimGetUserMedia} from './getusermedia'; 13 | 14 | export function shimMediaStream(window) { 15 | window.MediaStream = window.MediaStream || window.webkitMediaStream; 16 | } 17 | 18 | export function shimOnTrack(window) { 19 | if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in 20 | window.RTCPeerConnection.prototype)) { 21 | Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { 22 | get() { 23 | return this._ontrack; 24 | }, 25 | set(f) { 26 | if (this._ontrack) { 27 | this.removeEventListener('track', this._ontrack); 28 | } 29 | this.addEventListener('track', this._ontrack = f); 30 | }, 31 | enumerable: true, 32 | configurable: true 33 | }); 34 | const origSetRemoteDescription = 35 | window.RTCPeerConnection.prototype.setRemoteDescription; 36 | window.RTCPeerConnection.prototype.setRemoteDescription = 37 | function setRemoteDescription() { 38 | if (!this._ontrackpoly) { 39 | this._ontrackpoly = (e) => { 40 | // onaddstream does not fire when a track is added to an existing 41 | // stream. But stream.onaddtrack is implemented so we use that. 42 | e.stream.addEventListener('addtrack', te => { 43 | let receiver; 44 | if (window.RTCPeerConnection.prototype.getReceivers) { 45 | receiver = this.getReceivers() 46 | .find(r => r.track && r.track.id === te.track.id); 47 | } else { 48 | receiver = {track: te.track}; 49 | } 50 | 51 | const event = new Event('track'); 52 | event.track = te.track; 53 | event.receiver = receiver; 54 | event.transceiver = {receiver}; 55 | event.streams = [e.stream]; 56 | this.dispatchEvent(event); 57 | }); 58 | e.stream.getTracks().forEach(track => { 59 | let receiver; 60 | if (window.RTCPeerConnection.prototype.getReceivers) { 61 | receiver = this.getReceivers() 62 | .find(r => r.track && r.track.id === track.id); 63 | } else { 64 | receiver = {track}; 65 | } 66 | const event = new Event('track'); 67 | event.track = track; 68 | event.receiver = receiver; 69 | event.transceiver = {receiver}; 70 | event.streams = [e.stream]; 71 | this.dispatchEvent(event); 72 | }); 73 | }; 74 | this.addEventListener('addstream', this._ontrackpoly); 75 | } 76 | return origSetRemoteDescription.apply(this, arguments); 77 | }; 78 | } else { 79 | // even if RTCRtpTransceiver is in window, it is only used and 80 | // emitted in unified-plan. Unfortunately this means we need 81 | // to unconditionally wrap the event. 82 | utils.wrapPeerConnectionEvent(window, 'track', e => { 83 | if (!e.transceiver) { 84 | Object.defineProperty(e, 'transceiver', 85 | {value: {receiver: e.receiver}}); 86 | } 87 | return e; 88 | }); 89 | } 90 | } 91 | 92 | export function shimGetSendersWithDtmf(window) { 93 | // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack. 94 | if (typeof window === 'object' && window.RTCPeerConnection && 95 | !('getSenders' in window.RTCPeerConnection.prototype) && 96 | 'createDTMFSender' in window.RTCPeerConnection.prototype) { 97 | const shimSenderWithDtmf = function(pc, track) { 98 | return { 99 | track, 100 | get dtmf() { 101 | if (this._dtmf === undefined) { 102 | if (track.kind === 'audio') { 103 | this._dtmf = pc.createDTMFSender(track); 104 | } else { 105 | this._dtmf = null; 106 | } 107 | } 108 | return this._dtmf; 109 | }, 110 | _pc: pc 111 | }; 112 | }; 113 | 114 | // augment addTrack when getSenders is not available. 115 | if (!window.RTCPeerConnection.prototype.getSenders) { 116 | window.RTCPeerConnection.prototype.getSenders = function getSenders() { 117 | this._senders = this._senders || []; 118 | return this._senders.slice(); // return a copy of the internal state. 119 | }; 120 | const origAddTrack = window.RTCPeerConnection.prototype.addTrack; 121 | window.RTCPeerConnection.prototype.addTrack = 122 | function addTrack(track, stream) { 123 | let sender = origAddTrack.apply(this, arguments); 124 | if (!sender) { 125 | sender = shimSenderWithDtmf(this, track); 126 | this._senders.push(sender); 127 | } 128 | return sender; 129 | }; 130 | 131 | const origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; 132 | window.RTCPeerConnection.prototype.removeTrack = 133 | function removeTrack(sender) { 134 | origRemoveTrack.apply(this, arguments); 135 | const idx = this._senders.indexOf(sender); 136 | if (idx !== -1) { 137 | this._senders.splice(idx, 1); 138 | } 139 | }; 140 | } 141 | const origAddStream = window.RTCPeerConnection.prototype.addStream; 142 | window.RTCPeerConnection.prototype.addStream = function addStream(stream) { 143 | this._senders = this._senders || []; 144 | origAddStream.apply(this, [stream]); 145 | stream.getTracks().forEach(track => { 146 | this._senders.push(shimSenderWithDtmf(this, track)); 147 | }); 148 | }; 149 | 150 | const origRemoveStream = window.RTCPeerConnection.prototype.removeStream; 151 | window.RTCPeerConnection.prototype.removeStream = 152 | function removeStream(stream) { 153 | this._senders = this._senders || []; 154 | origRemoveStream.apply(this, [stream]); 155 | 156 | stream.getTracks().forEach(track => { 157 | const sender = this._senders.find(s => s.track === track); 158 | if (sender) { // remove sender 159 | this._senders.splice(this._senders.indexOf(sender), 1); 160 | } 161 | }); 162 | }; 163 | } else if (typeof window === 'object' && window.RTCPeerConnection && 164 | 'getSenders' in window.RTCPeerConnection.prototype && 165 | 'createDTMFSender' in window.RTCPeerConnection.prototype && 166 | window.RTCRtpSender && 167 | !('dtmf' in window.RTCRtpSender.prototype)) { 168 | const origGetSenders = window.RTCPeerConnection.prototype.getSenders; 169 | window.RTCPeerConnection.prototype.getSenders = function getSenders() { 170 | const senders = origGetSenders.apply(this, []); 171 | senders.forEach(sender => sender._pc = this); 172 | return senders; 173 | }; 174 | 175 | Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { 176 | get() { 177 | if (this._dtmf === undefined) { 178 | if (this.track.kind === 'audio') { 179 | this._dtmf = this._pc.createDTMFSender(this.track); 180 | } else { 181 | this._dtmf = null; 182 | } 183 | } 184 | return this._dtmf; 185 | } 186 | }); 187 | } 188 | } 189 | 190 | export function shimSenderReceiverGetStats(window) { 191 | if (!(typeof window === 'object' && window.RTCPeerConnection && 192 | window.RTCRtpSender && window.RTCRtpReceiver)) { 193 | return; 194 | } 195 | 196 | // shim sender stats. 197 | if (!('getStats' in window.RTCRtpSender.prototype)) { 198 | const origGetSenders = window.RTCPeerConnection.prototype.getSenders; 199 | if (origGetSenders) { 200 | window.RTCPeerConnection.prototype.getSenders = function getSenders() { 201 | const senders = origGetSenders.apply(this, []); 202 | senders.forEach(sender => sender._pc = this); 203 | return senders; 204 | }; 205 | } 206 | 207 | const origAddTrack = window.RTCPeerConnection.prototype.addTrack; 208 | if (origAddTrack) { 209 | window.RTCPeerConnection.prototype.addTrack = function addTrack() { 210 | const sender = origAddTrack.apply(this, arguments); 211 | sender._pc = this; 212 | return sender; 213 | }; 214 | } 215 | window.RTCRtpSender.prototype.getStats = function getStats() { 216 | const sender = this; 217 | return this._pc.getStats().then(result => 218 | /* Note: this will include stats of all senders that 219 | * send a track with the same id as sender.track as 220 | * it is not possible to identify the RTCRtpSender. 221 | */ 222 | utils.filterStats(result, sender.track, true)); 223 | }; 224 | } 225 | 226 | // shim receiver stats. 227 | if (!('getStats' in window.RTCRtpReceiver.prototype)) { 228 | const origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; 229 | if (origGetReceivers) { 230 | window.RTCPeerConnection.prototype.getReceivers = 231 | function getReceivers() { 232 | const receivers = origGetReceivers.apply(this, []); 233 | receivers.forEach(receiver => receiver._pc = this); 234 | return receivers; 235 | }; 236 | } 237 | utils.wrapPeerConnectionEvent(window, 'track', e => { 238 | e.receiver._pc = e.srcElement; 239 | return e; 240 | }); 241 | window.RTCRtpReceiver.prototype.getStats = function getStats() { 242 | const receiver = this; 243 | return this._pc.getStats().then(result => 244 | utils.filterStats(result, receiver.track, false)); 245 | }; 246 | } 247 | 248 | if (!('getStats' in window.RTCRtpSender.prototype && 249 | 'getStats' in window.RTCRtpReceiver.prototype)) { 250 | return; 251 | } 252 | 253 | // shim RTCPeerConnection.getStats(track). 254 | const origGetStats = window.RTCPeerConnection.prototype.getStats; 255 | window.RTCPeerConnection.prototype.getStats = function getStats() { 256 | if (arguments.length > 0 && 257 | arguments[0] instanceof window.MediaStreamTrack) { 258 | const track = arguments[0]; 259 | let sender; 260 | let receiver; 261 | let err; 262 | this.getSenders().forEach(s => { 263 | if (s.track === track) { 264 | if (sender) { 265 | err = true; 266 | } else { 267 | sender = s; 268 | } 269 | } 270 | }); 271 | this.getReceivers().forEach(r => { 272 | if (r.track === track) { 273 | if (receiver) { 274 | err = true; 275 | } else { 276 | receiver = r; 277 | } 278 | } 279 | return r.track === track; 280 | }); 281 | if (err || (sender && receiver)) { 282 | return Promise.reject(new DOMException( 283 | 'There are more than one sender or receiver for the track.', 284 | 'InvalidAccessError')); 285 | } else if (sender) { 286 | return sender.getStats(); 287 | } else if (receiver) { 288 | return receiver.getStats(); 289 | } 290 | return Promise.reject(new DOMException( 291 | 'There is no sender or receiver for the track.', 292 | 'InvalidAccessError')); 293 | } 294 | return origGetStats.apply(this, arguments); 295 | }; 296 | } 297 | 298 | export function shimAddTrackRemoveTrackWithNative(window) { 299 | // shim addTrack/removeTrack with native variants in order to make 300 | // the interactions with legacy getLocalStreams behave as in other browsers. 301 | // Keeps a mapping stream.id => [stream, rtpsenders...] 302 | window.RTCPeerConnection.prototype.getLocalStreams = 303 | function getLocalStreams() { 304 | this._shimmedLocalStreams = this._shimmedLocalStreams || {}; 305 | return Object.keys(this._shimmedLocalStreams) 306 | .map(streamId => this._shimmedLocalStreams[streamId][0]); 307 | }; 308 | 309 | const origAddTrack = window.RTCPeerConnection.prototype.addTrack; 310 | window.RTCPeerConnection.prototype.addTrack = 311 | function addTrack(track, stream) { 312 | if (!stream) { 313 | return origAddTrack.apply(this, arguments); 314 | } 315 | this._shimmedLocalStreams = this._shimmedLocalStreams || {}; 316 | 317 | const sender = origAddTrack.apply(this, arguments); 318 | if (!this._shimmedLocalStreams[stream.id]) { 319 | this._shimmedLocalStreams[stream.id] = [stream, sender]; 320 | } else if (this._shimmedLocalStreams[stream.id].indexOf(sender) === -1) { 321 | this._shimmedLocalStreams[stream.id].push(sender); 322 | } 323 | return sender; 324 | }; 325 | 326 | const origAddStream = window.RTCPeerConnection.prototype.addStream; 327 | window.RTCPeerConnection.prototype.addStream = function addStream(stream) { 328 | this._shimmedLocalStreams = this._shimmedLocalStreams || {}; 329 | 330 | stream.getTracks().forEach(track => { 331 | const alreadyExists = this.getSenders().find(s => s.track === track); 332 | if (alreadyExists) { 333 | throw new DOMException('Track already exists.', 334 | 'InvalidAccessError'); 335 | } 336 | }); 337 | const existingSenders = this.getSenders(); 338 | origAddStream.apply(this, arguments); 339 | const newSenders = this.getSenders() 340 | .filter(newSender => existingSenders.indexOf(newSender) === -1); 341 | this._shimmedLocalStreams[stream.id] = [stream].concat(newSenders); 342 | }; 343 | 344 | const origRemoveStream = window.RTCPeerConnection.prototype.removeStream; 345 | window.RTCPeerConnection.prototype.removeStream = 346 | function removeStream(stream) { 347 | this._shimmedLocalStreams = this._shimmedLocalStreams || {}; 348 | delete this._shimmedLocalStreams[stream.id]; 349 | return origRemoveStream.apply(this, arguments); 350 | }; 351 | 352 | const origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; 353 | window.RTCPeerConnection.prototype.removeTrack = 354 | function removeTrack(sender) { 355 | this._shimmedLocalStreams = this._shimmedLocalStreams || {}; 356 | if (sender) { 357 | Object.keys(this._shimmedLocalStreams).forEach(streamId => { 358 | const idx = this._shimmedLocalStreams[streamId].indexOf(sender); 359 | if (idx !== -1) { 360 | this._shimmedLocalStreams[streamId].splice(idx, 1); 361 | } 362 | if (this._shimmedLocalStreams[streamId].length === 1) { 363 | delete this._shimmedLocalStreams[streamId]; 364 | } 365 | }); 366 | } 367 | return origRemoveTrack.apply(this, arguments); 368 | }; 369 | } 370 | 371 | export function shimAddTrackRemoveTrack(window, browserDetails) { 372 | if (!window.RTCPeerConnection) { 373 | return; 374 | } 375 | // shim addTrack and removeTrack. 376 | if (window.RTCPeerConnection.prototype.addTrack && 377 | browserDetails.version >= 65) { 378 | return shimAddTrackRemoveTrackWithNative(window); 379 | } 380 | 381 | // also shim pc.getLocalStreams when addTrack is shimmed 382 | // to return the original streams. 383 | const origGetLocalStreams = window.RTCPeerConnection.prototype 384 | .getLocalStreams; 385 | window.RTCPeerConnection.prototype.getLocalStreams = 386 | function getLocalStreams() { 387 | const nativeStreams = origGetLocalStreams.apply(this); 388 | this._reverseStreams = this._reverseStreams || {}; 389 | return nativeStreams.map(stream => this._reverseStreams[stream.id]); 390 | }; 391 | 392 | const origAddStream = window.RTCPeerConnection.prototype.addStream; 393 | window.RTCPeerConnection.prototype.addStream = function addStream(stream) { 394 | this._streams = this._streams || {}; 395 | this._reverseStreams = this._reverseStreams || {}; 396 | 397 | stream.getTracks().forEach(track => { 398 | const alreadyExists = this.getSenders().find(s => s.track === track); 399 | if (alreadyExists) { 400 | throw new DOMException('Track already exists.', 401 | 'InvalidAccessError'); 402 | } 403 | }); 404 | // Add identity mapping for consistency with addTrack. 405 | // Unless this is being used with a stream from addTrack. 406 | if (!this._reverseStreams[stream.id]) { 407 | const newStream = new window.MediaStream(stream.getTracks()); 408 | this._streams[stream.id] = newStream; 409 | this._reverseStreams[newStream.id] = stream; 410 | stream = newStream; 411 | } 412 | origAddStream.apply(this, [stream]); 413 | }; 414 | 415 | const origRemoveStream = window.RTCPeerConnection.prototype.removeStream; 416 | window.RTCPeerConnection.prototype.removeStream = 417 | function removeStream(stream) { 418 | this._streams = this._streams || {}; 419 | this._reverseStreams = this._reverseStreams || {}; 420 | 421 | origRemoveStream.apply(this, [(this._streams[stream.id] || stream)]); 422 | delete this._reverseStreams[(this._streams[stream.id] ? 423 | this._streams[stream.id].id : stream.id)]; 424 | delete this._streams[stream.id]; 425 | }; 426 | 427 | window.RTCPeerConnection.prototype.addTrack = 428 | function addTrack(track, stream) { 429 | if (this.signalingState === 'closed') { 430 | throw new DOMException( 431 | 'The RTCPeerConnection\'s signalingState is \'closed\'.', 432 | 'InvalidStateError'); 433 | } 434 | const streams = [].slice.call(arguments, 1); 435 | if (streams.length !== 1 || 436 | !streams[0].getTracks().find(t => t === track)) { 437 | // this is not fully correct but all we can manage without 438 | // [[associated MediaStreams]] internal slot. 439 | throw new DOMException( 440 | 'The adapter.js addTrack polyfill only supports a single ' + 441 | ' stream which is associated with the specified track.', 442 | 'NotSupportedError'); 443 | } 444 | 445 | const alreadyExists = this.getSenders().find(s => s.track === track); 446 | if (alreadyExists) { 447 | throw new DOMException('Track already exists.', 448 | 'InvalidAccessError'); 449 | } 450 | 451 | this._streams = this._streams || {}; 452 | this._reverseStreams = this._reverseStreams || {}; 453 | const oldStream = this._streams[stream.id]; 454 | if (oldStream) { 455 | // this is using odd Chrome behaviour, use with caution: 456 | // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815 457 | // Note: we rely on the high-level addTrack/dtmf shim to 458 | // create the sender with a dtmf sender. 459 | oldStream.addTrack(track); 460 | 461 | // Trigger ONN async. 462 | Promise.resolve().then(() => { 463 | this.dispatchEvent(new Event('negotiationneeded')); 464 | }); 465 | } else { 466 | const newStream = new window.MediaStream([track]); 467 | this._streams[stream.id] = newStream; 468 | this._reverseStreams[newStream.id] = stream; 469 | this.addStream(newStream); 470 | } 471 | return this.getSenders().find(s => s.track === track); 472 | }; 473 | 474 | // replace the internal stream id with the external one and 475 | // vice versa. 476 | function replaceInternalStreamId(pc, description) { 477 | let sdp = description.sdp; 478 | Object.keys(pc._reverseStreams || []).forEach(internalId => { 479 | const externalStream = pc._reverseStreams[internalId]; 480 | const internalStream = pc._streams[externalStream.id]; 481 | sdp = sdp.replace(new RegExp(internalStream.id, 'g'), 482 | externalStream.id); 483 | }); 484 | return new RTCSessionDescription({ 485 | type: description.type, 486 | sdp 487 | }); 488 | } 489 | function replaceExternalStreamId(pc, description) { 490 | let sdp = description.sdp; 491 | Object.keys(pc._reverseStreams || []).forEach(internalId => { 492 | const externalStream = pc._reverseStreams[internalId]; 493 | const internalStream = pc._streams[externalStream.id]; 494 | sdp = sdp.replace(new RegExp(externalStream.id, 'g'), 495 | internalStream.id); 496 | }); 497 | return new RTCSessionDescription({ 498 | type: description.type, 499 | sdp 500 | }); 501 | } 502 | ['createOffer', 'createAnswer'].forEach(function(method) { 503 | const nativeMethod = window.RTCPeerConnection.prototype[method]; 504 | const methodObj = {[method]() { 505 | const args = arguments; 506 | const isLegacyCall = arguments.length && 507 | typeof arguments[0] === 'function'; 508 | if (isLegacyCall) { 509 | return nativeMethod.apply(this, [ 510 | (description) => { 511 | const desc = replaceInternalStreamId(this, description); 512 | args[0].apply(null, [desc]); 513 | }, 514 | (err) => { 515 | if (args[1]) { 516 | args[1].apply(null, err); 517 | } 518 | }, arguments[2] 519 | ]); 520 | } 521 | return nativeMethod.apply(this, arguments) 522 | .then(description => replaceInternalStreamId(this, description)); 523 | }}; 524 | window.RTCPeerConnection.prototype[method] = methodObj[method]; 525 | }); 526 | 527 | const origSetLocalDescription = 528 | window.RTCPeerConnection.prototype.setLocalDescription; 529 | window.RTCPeerConnection.prototype.setLocalDescription = 530 | function setLocalDescription() { 531 | if (!arguments.length || !arguments[0].type) { 532 | return origSetLocalDescription.apply(this, arguments); 533 | } 534 | arguments[0] = replaceExternalStreamId(this, arguments[0]); 535 | return origSetLocalDescription.apply(this, arguments); 536 | }; 537 | 538 | // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier 539 | 540 | const origLocalDescription = Object.getOwnPropertyDescriptor( 541 | window.RTCPeerConnection.prototype, 'localDescription'); 542 | Object.defineProperty(window.RTCPeerConnection.prototype, 543 | 'localDescription', { 544 | get() { 545 | const description = origLocalDescription.get.apply(this); 546 | if (description.type === '') { 547 | return description; 548 | } 549 | return replaceInternalStreamId(this, description); 550 | } 551 | }); 552 | 553 | window.RTCPeerConnection.prototype.removeTrack = 554 | function removeTrack(sender) { 555 | if (this.signalingState === 'closed') { 556 | throw new DOMException( 557 | 'The RTCPeerConnection\'s signalingState is \'closed\'.', 558 | 'InvalidStateError'); 559 | } 560 | // We can not yet check for sender instanceof RTCRtpSender 561 | // since we shim RTPSender. So we check if sender._pc is set. 562 | if (!sender._pc) { 563 | throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' + 564 | 'does not implement interface RTCRtpSender.', 'TypeError'); 565 | } 566 | const isLocal = sender._pc === this; 567 | if (!isLocal) { 568 | throw new DOMException('Sender was not created by this connection.', 569 | 'InvalidAccessError'); 570 | } 571 | 572 | // Search for the native stream the senders track belongs to. 573 | this._streams = this._streams || {}; 574 | let stream; 575 | Object.keys(this._streams).forEach(streamid => { 576 | const hasTrack = this._streams[streamid].getTracks() 577 | .find(track => sender.track === track); 578 | if (hasTrack) { 579 | stream = this._streams[streamid]; 580 | } 581 | }); 582 | 583 | if (stream) { 584 | if (stream.getTracks().length === 1) { 585 | // if this is the last track of the stream, remove the stream. This 586 | // takes care of any shimmed _senders. 587 | this.removeStream(this._reverseStreams[stream.id]); 588 | } else { 589 | // relying on the same odd chrome behaviour as above. 590 | stream.removeTrack(sender.track); 591 | } 592 | this.dispatchEvent(new Event('negotiationneeded')); 593 | } 594 | }; 595 | } 596 | 597 | export function shimPeerConnection(window, browserDetails) { 598 | if (!window.RTCPeerConnection && window.webkitRTCPeerConnection) { 599 | // very basic support for old versions. 600 | window.RTCPeerConnection = window.webkitRTCPeerConnection; 601 | } 602 | if (!window.RTCPeerConnection) { 603 | return; 604 | } 605 | 606 | // shim implicit creation of RTCSessionDescription/RTCIceCandidate 607 | if (browserDetails.version < 53) { 608 | ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] 609 | .forEach(function(method) { 610 | const nativeMethod = window.RTCPeerConnection.prototype[method]; 611 | const methodObj = {[method]() { 612 | arguments[0] = new ((method === 'addIceCandidate') ? 613 | window.RTCIceCandidate : 614 | window.RTCSessionDescription)(arguments[0]); 615 | return nativeMethod.apply(this, arguments); 616 | }}; 617 | window.RTCPeerConnection.prototype[method] = methodObj[method]; 618 | }); 619 | } 620 | } 621 | 622 | // Attempt to fix ONN in plan-b mode. 623 | export function fixNegotiationNeeded(window, browserDetails) { 624 | utils.wrapPeerConnectionEvent(window, 'negotiationneeded', e => { 625 | const pc = e.target; 626 | if (browserDetails.version < 72 || (pc.getConfiguration && 627 | pc.getConfiguration().sdpSemantics === 'plan-b')) { 628 | if (pc.signalingState !== 'stable') { 629 | return; 630 | } 631 | } 632 | return e; 633 | }); 634 | } 635 | -------------------------------------------------------------------------------- /src/js/chrome/getusermedia.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | import * as utils from '../utils.js'; 11 | const logging = utils.log; 12 | 13 | export function shimGetUserMedia(window, browserDetails) { 14 | const navigator = window && window.navigator; 15 | 16 | if (!navigator.mediaDevices) { 17 | return; 18 | } 19 | 20 | const constraintsToChrome_ = function(c) { 21 | if (typeof c !== 'object' || c.mandatory || c.optional) { 22 | return c; 23 | } 24 | const cc = {}; 25 | Object.keys(c).forEach(key => { 26 | if (key === 'require' || key === 'advanced' || key === 'mediaSource') { 27 | return; 28 | } 29 | const r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; 30 | if (r.exact !== undefined && typeof r.exact === 'number') { 31 | r.min = r.max = r.exact; 32 | } 33 | const oldname_ = function(prefix, name) { 34 | if (prefix) { 35 | return prefix + name.charAt(0).toUpperCase() + name.slice(1); 36 | } 37 | return (name === 'deviceId') ? 'sourceId' : name; 38 | }; 39 | if (r.ideal !== undefined) { 40 | cc.optional = cc.optional || []; 41 | let oc = {}; 42 | if (typeof r.ideal === 'number') { 43 | oc[oldname_('min', key)] = r.ideal; 44 | cc.optional.push(oc); 45 | oc = {}; 46 | oc[oldname_('max', key)] = r.ideal; 47 | cc.optional.push(oc); 48 | } else { 49 | oc[oldname_('', key)] = r.ideal; 50 | cc.optional.push(oc); 51 | } 52 | } 53 | if (r.exact !== undefined && typeof r.exact !== 'number') { 54 | cc.mandatory = cc.mandatory || {}; 55 | cc.mandatory[oldname_('', key)] = r.exact; 56 | } else { 57 | ['min', 'max'].forEach(mix => { 58 | if (r[mix] !== undefined) { 59 | cc.mandatory = cc.mandatory || {}; 60 | cc.mandatory[oldname_(mix, key)] = r[mix]; 61 | } 62 | }); 63 | } 64 | }); 65 | if (c.advanced) { 66 | cc.optional = (cc.optional || []).concat(c.advanced); 67 | } 68 | return cc; 69 | }; 70 | 71 | const shimConstraints_ = function(constraints, func) { 72 | if (browserDetails.version >= 61) { 73 | return func(constraints); 74 | } 75 | constraints = JSON.parse(JSON.stringify(constraints)); 76 | if (constraints && typeof constraints.audio === 'object') { 77 | const remap = function(obj, a, b) { 78 | if (a in obj && !(b in obj)) { 79 | obj[b] = obj[a]; 80 | delete obj[a]; 81 | } 82 | }; 83 | constraints = JSON.parse(JSON.stringify(constraints)); 84 | remap(constraints.audio, 'autoGainControl', 'googAutoGainControl'); 85 | remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression'); 86 | constraints.audio = constraintsToChrome_(constraints.audio); 87 | } 88 | if (constraints && typeof constraints.video === 'object') { 89 | // Shim facingMode for mobile & surface pro. 90 | let face = constraints.video.facingMode; 91 | face = face && ((typeof face === 'object') ? face : {ideal: face}); 92 | const getSupportedFacingModeLies = browserDetails.version < 66; 93 | 94 | if ((face && (face.exact === 'user' || face.exact === 'environment' || 95 | face.ideal === 'user' || face.ideal === 'environment')) && 96 | !(navigator.mediaDevices.getSupportedConstraints && 97 | navigator.mediaDevices.getSupportedConstraints().facingMode && 98 | !getSupportedFacingModeLies)) { 99 | delete constraints.video.facingMode; 100 | let matches; 101 | if (face.exact === 'environment' || face.ideal === 'environment') { 102 | matches = ['back', 'rear']; 103 | } else if (face.exact === 'user' || face.ideal === 'user') { 104 | matches = ['front']; 105 | } 106 | if (matches) { 107 | // Look for matches in label, or use last cam for back (typical). 108 | return navigator.mediaDevices.enumerateDevices() 109 | .then(devices => { 110 | devices = devices.filter(d => d.kind === 'videoinput'); 111 | let dev = devices.find(d => matches.some(match => 112 | d.label.toLowerCase().includes(match))); 113 | if (!dev && devices.length && matches.includes('back')) { 114 | dev = devices[devices.length - 1]; // more likely the back cam 115 | } 116 | if (dev) { 117 | constraints.video.deviceId = face.exact 118 | ? {exact: dev.deviceId} 119 | : {ideal: dev.deviceId}; 120 | } 121 | constraints.video = constraintsToChrome_(constraints.video); 122 | logging('chrome: ' + JSON.stringify(constraints)); 123 | return func(constraints); 124 | }); 125 | } 126 | } 127 | constraints.video = constraintsToChrome_(constraints.video); 128 | } 129 | logging('chrome: ' + JSON.stringify(constraints)); 130 | return func(constraints); 131 | }; 132 | 133 | const shimError_ = function(e) { 134 | if (browserDetails.version >= 64) { 135 | return e; 136 | } 137 | return { 138 | name: { 139 | PermissionDeniedError: 'NotAllowedError', 140 | PermissionDismissedError: 'NotAllowedError', 141 | InvalidStateError: 'NotAllowedError', 142 | DevicesNotFoundError: 'NotFoundError', 143 | ConstraintNotSatisfiedError: 'OverconstrainedError', 144 | TrackStartError: 'NotReadableError', 145 | MediaDeviceFailedDueToShutdown: 'NotAllowedError', 146 | MediaDeviceKillSwitchOn: 'NotAllowedError', 147 | TabCaptureError: 'AbortError', 148 | ScreenCaptureError: 'AbortError', 149 | DeviceCaptureError: 'AbortError' 150 | }[e.name] || e.name, 151 | message: e.message, 152 | constraint: e.constraint || e.constraintName, 153 | toString() { 154 | return this.name + (this.message && ': ') + this.message; 155 | } 156 | }; 157 | }; 158 | 159 | const getUserMedia_ = function(constraints, onSuccess, onError) { 160 | shimConstraints_(constraints, c => { 161 | navigator.webkitGetUserMedia(c, onSuccess, e => { 162 | if (onError) { 163 | onError(shimError_(e)); 164 | } 165 | }); 166 | }); 167 | }; 168 | navigator.getUserMedia = getUserMedia_.bind(navigator); 169 | 170 | // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia 171 | // function which returns a Promise, it does not accept spec-style 172 | // constraints. 173 | if (navigator.mediaDevices.getUserMedia) { 174 | const origGetUserMedia = navigator.mediaDevices.getUserMedia. 175 | bind(navigator.mediaDevices); 176 | navigator.mediaDevices.getUserMedia = function(cs) { 177 | return shimConstraints_(cs, c => origGetUserMedia(c).then(stream => { 178 | if (c.audio && !stream.getAudioTracks().length || 179 | c.video && !stream.getVideoTracks().length) { 180 | stream.getTracks().forEach(track => { 181 | track.stop(); 182 | }); 183 | throw new DOMException('', 'NotFoundError'); 184 | } 185 | return stream; 186 | }, e => Promise.reject(shimError_(e)))); 187 | }; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/js/common_shim.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | import SDPUtils from 'sdp'; 12 | import * as utils from './utils'; 13 | 14 | export function shimRTCIceCandidate(window) { 15 | // foundation is arbitrarily chosen as an indicator for full support for 16 | // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface 17 | if (!window.RTCIceCandidate || (window.RTCIceCandidate && 'foundation' in 18 | window.RTCIceCandidate.prototype)) { 19 | return; 20 | } 21 | 22 | const NativeRTCIceCandidate = window.RTCIceCandidate; 23 | window.RTCIceCandidate = function RTCIceCandidate(args) { 24 | // Remove the a= which shouldn't be part of the candidate string. 25 | if (typeof args === 'object' && args.candidate && 26 | args.candidate.indexOf('a=') === 0) { 27 | args = JSON.parse(JSON.stringify(args)); 28 | args.candidate = args.candidate.substring(2); 29 | } 30 | 31 | if (args.candidate && args.candidate.length) { 32 | // Augment the native candidate with the parsed fields. 33 | const nativeCandidate = new NativeRTCIceCandidate(args); 34 | const parsedCandidate = SDPUtils.parseCandidate(args.candidate); 35 | for (const key in parsedCandidate) { 36 | if (!(key in nativeCandidate)) { 37 | Object.defineProperty(nativeCandidate, key, 38 | {value: parsedCandidate[key]}); 39 | } 40 | } 41 | 42 | // Override serializer to not serialize the extra attributes. 43 | nativeCandidate.toJSON = function toJSON() { 44 | return { 45 | candidate: nativeCandidate.candidate, 46 | sdpMid: nativeCandidate.sdpMid, 47 | sdpMLineIndex: nativeCandidate.sdpMLineIndex, 48 | usernameFragment: nativeCandidate.usernameFragment, 49 | }; 50 | }; 51 | return nativeCandidate; 52 | } 53 | return new NativeRTCIceCandidate(args); 54 | }; 55 | window.RTCIceCandidate.prototype = NativeRTCIceCandidate.prototype; 56 | 57 | // Hook up the augmented candidate in onicecandidate and 58 | // addEventListener('icecandidate', ...) 59 | utils.wrapPeerConnectionEvent(window, 'icecandidate', e => { 60 | if (e.candidate) { 61 | Object.defineProperty(e, 'candidate', { 62 | value: new window.RTCIceCandidate(e.candidate), 63 | writable: 'false' 64 | }); 65 | } 66 | return e; 67 | }); 68 | } 69 | 70 | export function shimRTCIceCandidateRelayProtocol(window) { 71 | if (!window.RTCIceCandidate || (window.RTCIceCandidate && 'relayProtocol' in 72 | window.RTCIceCandidate.prototype)) { 73 | return; 74 | } 75 | 76 | // Hook up the augmented candidate in onicecandidate and 77 | // addEventListener('icecandidate', ...) 78 | utils.wrapPeerConnectionEvent(window, 'icecandidate', e => { 79 | if (e.candidate) { 80 | const parsedCandidate = SDPUtils.parseCandidate(e.candidate.candidate); 81 | if (parsedCandidate.type === 'relay') { 82 | // This is a libwebrtc-specific mapping of local type preference 83 | // to relayProtocol. 84 | e.candidate.relayProtocol = { 85 | 0: 'tls', 86 | 1: 'tcp', 87 | 2: 'udp', 88 | }[parsedCandidate.priority >> 24]; 89 | } 90 | } 91 | return e; 92 | }); 93 | } 94 | 95 | export function shimMaxMessageSize(window, browserDetails) { 96 | if (!window.RTCPeerConnection) { 97 | return; 98 | } 99 | 100 | if (!('sctp' in window.RTCPeerConnection.prototype)) { 101 | Object.defineProperty(window.RTCPeerConnection.prototype, 'sctp', { 102 | get() { 103 | return typeof this._sctp === 'undefined' ? null : this._sctp; 104 | } 105 | }); 106 | } 107 | 108 | const sctpInDescription = function(description) { 109 | if (!description || !description.sdp) { 110 | return false; 111 | } 112 | const sections = SDPUtils.splitSections(description.sdp); 113 | sections.shift(); 114 | return sections.some(mediaSection => { 115 | const mLine = SDPUtils.parseMLine(mediaSection); 116 | return mLine && mLine.kind === 'application' 117 | && mLine.protocol.indexOf('SCTP') !== -1; 118 | }); 119 | }; 120 | 121 | const getRemoteFirefoxVersion = function(description) { 122 | // TODO: Is there a better solution for detecting Firefox? 123 | const match = description.sdp.match(/mozilla...THIS_IS_SDPARTA-(\d+)/); 124 | if (match === null || match.length < 2) { 125 | return -1; 126 | } 127 | const version = parseInt(match[1], 10); 128 | // Test for NaN (yes, this is ugly) 129 | return version !== version ? -1 : version; 130 | }; 131 | 132 | const getCanSendMaxMessageSize = function(remoteIsFirefox) { 133 | // Every implementation we know can send at least 64 KiB. 134 | // Note: Although Chrome is technically able to send up to 256 KiB, the 135 | // data does not reach the other peer reliably. 136 | // See: https://bugs.chromium.org/p/webrtc/issues/detail?id=8419 137 | let canSendMaxMessageSize = 65536; 138 | if (browserDetails.browser === 'firefox') { 139 | if (browserDetails.version < 57) { 140 | if (remoteIsFirefox === -1) { 141 | // FF < 57 will send in 16 KiB chunks using the deprecated PPID 142 | // fragmentation. 143 | canSendMaxMessageSize = 16384; 144 | } else { 145 | // However, other FF (and RAWRTC) can reassemble PPID-fragmented 146 | // messages. Thus, supporting ~2 GiB when sending. 147 | canSendMaxMessageSize = 2147483637; 148 | } 149 | } else if (browserDetails.version < 60) { 150 | // Currently, all FF >= 57 will reset the remote maximum message size 151 | // to the default value when a data channel is created at a later 152 | // stage. :( 153 | // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 154 | canSendMaxMessageSize = 155 | browserDetails.version === 57 ? 65535 : 65536; 156 | } else { 157 | // FF >= 60 supports sending ~2 GiB 158 | canSendMaxMessageSize = 2147483637; 159 | } 160 | } 161 | return canSendMaxMessageSize; 162 | }; 163 | 164 | const getMaxMessageSize = function(description, remoteIsFirefox) { 165 | // Note: 65536 bytes is the default value from the SDP spec. Also, 166 | // every implementation we know supports receiving 65536 bytes. 167 | let maxMessageSize = 65536; 168 | 169 | // FF 57 has a slightly incorrect default remote max message size, so 170 | // we need to adjust it here to avoid a failure when sending. 171 | // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1425697 172 | if (browserDetails.browser === 'firefox' 173 | && browserDetails.version === 57) { 174 | maxMessageSize = 65535; 175 | } 176 | 177 | const match = SDPUtils.matchPrefix(description.sdp, 178 | 'a=max-message-size:'); 179 | if (match.length > 0) { 180 | maxMessageSize = parseInt(match[0].substring(19), 10); 181 | } else if (browserDetails.browser === 'firefox' && 182 | remoteIsFirefox !== -1) { 183 | // If the maximum message size is not present in the remote SDP and 184 | // both local and remote are Firefox, the remote peer can receive 185 | // ~2 GiB. 186 | maxMessageSize = 2147483637; 187 | } 188 | return maxMessageSize; 189 | }; 190 | 191 | const origSetRemoteDescription = 192 | window.RTCPeerConnection.prototype.setRemoteDescription; 193 | window.RTCPeerConnection.prototype.setRemoteDescription = 194 | function setRemoteDescription() { 195 | this._sctp = null; 196 | // Chrome decided to not expose .sctp in plan-b mode. 197 | // As usual, adapter.js has to do an 'ugly worakaround' 198 | // to cover up the mess. 199 | if (browserDetails.browser === 'chrome' && browserDetails.version >= 76) { 200 | const {sdpSemantics} = this.getConfiguration(); 201 | if (sdpSemantics === 'plan-b') { 202 | Object.defineProperty(this, 'sctp', { 203 | get() { 204 | return typeof this._sctp === 'undefined' ? null : this._sctp; 205 | }, 206 | enumerable: true, 207 | configurable: true, 208 | }); 209 | } 210 | } 211 | 212 | if (sctpInDescription(arguments[0])) { 213 | // Check if the remote is FF. 214 | const isFirefox = getRemoteFirefoxVersion(arguments[0]); 215 | 216 | // Get the maximum message size the local peer is capable of sending 217 | const canSendMMS = getCanSendMaxMessageSize(isFirefox); 218 | 219 | // Get the maximum message size of the remote peer. 220 | const remoteMMS = getMaxMessageSize(arguments[0], isFirefox); 221 | 222 | // Determine final maximum message size 223 | let maxMessageSize; 224 | if (canSendMMS === 0 && remoteMMS === 0) { 225 | maxMessageSize = Number.POSITIVE_INFINITY; 226 | } else if (canSendMMS === 0 || remoteMMS === 0) { 227 | maxMessageSize = Math.max(canSendMMS, remoteMMS); 228 | } else { 229 | maxMessageSize = Math.min(canSendMMS, remoteMMS); 230 | } 231 | 232 | // Create a dummy RTCSctpTransport object and the 'maxMessageSize' 233 | // attribute. 234 | const sctp = {}; 235 | Object.defineProperty(sctp, 'maxMessageSize', { 236 | get() { 237 | return maxMessageSize; 238 | } 239 | }); 240 | this._sctp = sctp; 241 | } 242 | 243 | return origSetRemoteDescription.apply(this, arguments); 244 | }; 245 | } 246 | 247 | export function shimSendThrowTypeError(window) { 248 | if (!(window.RTCPeerConnection && 249 | 'createDataChannel' in window.RTCPeerConnection.prototype)) { 250 | return; 251 | } 252 | 253 | // Note: Although Firefox >= 57 has a native implementation, the maximum 254 | // message size can be reset for all data channels at a later stage. 255 | // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 256 | 257 | function wrapDcSend(dc, pc) { 258 | const origDataChannelSend = dc.send; 259 | dc.send = function send() { 260 | const data = arguments[0]; 261 | const length = data.length || data.size || data.byteLength; 262 | if (dc.readyState === 'open' && 263 | pc.sctp && length > pc.sctp.maxMessageSize) { 264 | throw new TypeError('Message too large (can send a maximum of ' + 265 | pc.sctp.maxMessageSize + ' bytes)'); 266 | } 267 | return origDataChannelSend.apply(dc, arguments); 268 | }; 269 | } 270 | const origCreateDataChannel = 271 | window.RTCPeerConnection.prototype.createDataChannel; 272 | window.RTCPeerConnection.prototype.createDataChannel = 273 | function createDataChannel() { 274 | const dataChannel = origCreateDataChannel.apply(this, arguments); 275 | wrapDcSend(dataChannel, this); 276 | return dataChannel; 277 | }; 278 | utils.wrapPeerConnectionEvent(window, 'datachannel', e => { 279 | wrapDcSend(e.channel, e.target); 280 | return e; 281 | }); 282 | } 283 | 284 | 285 | /* shims RTCConnectionState by pretending it is the same as iceConnectionState. 286 | * See https://bugs.chromium.org/p/webrtc/issues/detail?id=6145#c12 287 | * for why this is a valid hack in Chrome. In Firefox it is slightly incorrect 288 | * since DTLS failures would be hidden. See 289 | * https://bugzilla.mozilla.org/show_bug.cgi?id=1265827 290 | * for the Firefox tracking bug. 291 | */ 292 | export function shimConnectionState(window) { 293 | if (!window.RTCPeerConnection || 294 | 'connectionState' in window.RTCPeerConnection.prototype) { 295 | return; 296 | } 297 | const proto = window.RTCPeerConnection.prototype; 298 | Object.defineProperty(proto, 'connectionState', { 299 | get() { 300 | return { 301 | completed: 'connected', 302 | checking: 'connecting' 303 | }[this.iceConnectionState] || this.iceConnectionState; 304 | }, 305 | enumerable: true, 306 | configurable: true 307 | }); 308 | Object.defineProperty(proto, 'onconnectionstatechange', { 309 | get() { 310 | return this._onconnectionstatechange || null; 311 | }, 312 | set(cb) { 313 | if (this._onconnectionstatechange) { 314 | this.removeEventListener('connectionstatechange', 315 | this._onconnectionstatechange); 316 | delete this._onconnectionstatechange; 317 | } 318 | if (cb) { 319 | this.addEventListener('connectionstatechange', 320 | this._onconnectionstatechange = cb); 321 | } 322 | }, 323 | enumerable: true, 324 | configurable: true 325 | }); 326 | 327 | ['setLocalDescription', 'setRemoteDescription'].forEach((method) => { 328 | const origMethod = proto[method]; 329 | proto[method] = function() { 330 | if (!this._connectionstatechangepoly) { 331 | this._connectionstatechangepoly = e => { 332 | const pc = e.target; 333 | if (pc._lastConnectionState !== pc.connectionState) { 334 | pc._lastConnectionState = pc.connectionState; 335 | const newEvent = new Event('connectionstatechange', e); 336 | pc.dispatchEvent(newEvent); 337 | } 338 | return e; 339 | }; 340 | this.addEventListener('iceconnectionstatechange', 341 | this._connectionstatechangepoly); 342 | } 343 | return origMethod.apply(this, arguments); 344 | }; 345 | }); 346 | } 347 | 348 | export function removeExtmapAllowMixed(window, browserDetails) { 349 | /* remove a=extmap-allow-mixed for webrtc.org < M71 */ 350 | if (!window.RTCPeerConnection) { 351 | return; 352 | } 353 | if (browserDetails.browser === 'chrome' && browserDetails.version >= 71) { 354 | return; 355 | } 356 | if (browserDetails.browser === 'safari' && browserDetails.version >= 605) { 357 | return; 358 | } 359 | const nativeSRD = window.RTCPeerConnection.prototype.setRemoteDescription; 360 | window.RTCPeerConnection.prototype.setRemoteDescription = 361 | function setRemoteDescription(desc) { 362 | if (desc && desc.sdp && desc.sdp.indexOf('\na=extmap-allow-mixed') !== -1) { 363 | const sdp = desc.sdp.split('\n').filter((line) => { 364 | return line.trim() !== 'a=extmap-allow-mixed'; 365 | }).join('\n'); 366 | // Safari enforces read-only-ness of RTCSessionDescription fields. 367 | if (window.RTCSessionDescription && 368 | desc instanceof window.RTCSessionDescription) { 369 | arguments[0] = new window.RTCSessionDescription({ 370 | type: desc.type, 371 | sdp, 372 | }); 373 | } else { 374 | desc.sdp = sdp; 375 | } 376 | } 377 | return nativeSRD.apply(this, arguments); 378 | }; 379 | } 380 | 381 | export function shimAddIceCandidateNullOrEmpty(window, browserDetails) { 382 | // Support for addIceCandidate(null or undefined) 383 | // as well as addIceCandidate({candidate: "", ...}) 384 | // https://bugs.chromium.org/p/chromium/issues/detail?id=978582 385 | // Note: must be called before other polyfills which change the signature. 386 | if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) { 387 | return; 388 | } 389 | const nativeAddIceCandidate = 390 | window.RTCPeerConnection.prototype.addIceCandidate; 391 | if (!nativeAddIceCandidate || nativeAddIceCandidate.length === 0) { 392 | return; 393 | } 394 | window.RTCPeerConnection.prototype.addIceCandidate = 395 | function addIceCandidate() { 396 | if (!arguments[0]) { 397 | if (arguments[1]) { 398 | arguments[1].apply(null); 399 | } 400 | return Promise.resolve(); 401 | } 402 | // Firefox 68+ emits and processes {candidate: "", ...}, ignore 403 | // in older versions. 404 | // Native support for ignoring exists for Chrome M77+. 405 | // Safari ignores as well, exact version unknown but works in the same 406 | // version that also ignores addIceCandidate(null). 407 | if (((browserDetails.browser === 'chrome' && browserDetails.version < 78) 408 | || (browserDetails.browser === 'firefox' 409 | && browserDetails.version < 68) 410 | || (browserDetails.browser === 'safari')) 411 | && arguments[0] && arguments[0].candidate === '') { 412 | return Promise.resolve(); 413 | } 414 | return nativeAddIceCandidate.apply(this, arguments); 415 | }; 416 | } 417 | 418 | // Note: Make sure to call this ahead of APIs that modify 419 | // setLocalDescription.length 420 | export function shimParameterlessSetLocalDescription(window, browserDetails) { 421 | if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) { 422 | return; 423 | } 424 | const nativeSetLocalDescription = 425 | window.RTCPeerConnection.prototype.setLocalDescription; 426 | if (!nativeSetLocalDescription || nativeSetLocalDescription.length === 0) { 427 | return; 428 | } 429 | window.RTCPeerConnection.prototype.setLocalDescription = 430 | function setLocalDescription() { 431 | let desc = arguments[0] || {}; 432 | if (typeof desc !== 'object' || (desc.type && desc.sdp)) { 433 | return nativeSetLocalDescription.apply(this, arguments); 434 | } 435 | // The remaining steps should technically happen when SLD comes off the 436 | // RTCPeerConnection's operations chain (not ahead of going on it), but 437 | // this is too difficult to shim. Instead, this shim only covers the 438 | // common case where the operations chain is empty. This is imperfect, but 439 | // should cover many cases. Rationale: Even if we can't reduce the glare 440 | // window to zero on imperfect implementations, there's value in tapping 441 | // into the perfect negotiation pattern that several browsers support. 442 | desc = {type: desc.type, sdp: desc.sdp}; 443 | if (!desc.type) { 444 | switch (this.signalingState) { 445 | case 'stable': 446 | case 'have-local-offer': 447 | case 'have-remote-pranswer': 448 | desc.type = 'offer'; 449 | break; 450 | default: 451 | desc.type = 'answer'; 452 | break; 453 | } 454 | } 455 | if (desc.sdp || (desc.type !== 'offer' && desc.type !== 'answer')) { 456 | return nativeSetLocalDescription.apply(this, [desc]); 457 | } 458 | const func = desc.type === 'offer' ? this.createOffer : this.createAnswer; 459 | return func.apply(this) 460 | .then(d => nativeSetLocalDescription.apply(this, [d])); 461 | }; 462 | } 463 | -------------------------------------------------------------------------------- /src/js/firefox/firefox_shim.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | import * as utils from '../utils'; 12 | export {shimGetUserMedia} from './getusermedia'; 13 | export {shimGetDisplayMedia} from './getdisplaymedia'; 14 | 15 | export function shimOnTrack(window) { 16 | if (typeof window === 'object' && window.RTCTrackEvent && 17 | ('receiver' in window.RTCTrackEvent.prototype) && 18 | !('transceiver' in window.RTCTrackEvent.prototype)) { 19 | Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { 20 | get() { 21 | return {receiver: this.receiver}; 22 | } 23 | }); 24 | } 25 | } 26 | 27 | export function shimPeerConnection(window, browserDetails) { 28 | if (typeof window !== 'object' || 29 | !(window.RTCPeerConnection || window.mozRTCPeerConnection)) { 30 | return; // probably media.peerconnection.enabled=false in about:config 31 | } 32 | if (!window.RTCPeerConnection && window.mozRTCPeerConnection) { 33 | // very basic support for old versions. 34 | window.RTCPeerConnection = window.mozRTCPeerConnection; 35 | } 36 | 37 | if (browserDetails.version < 53) { 38 | // shim away need for obsolete RTCIceCandidate/RTCSessionDescription. 39 | ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] 40 | .forEach(function(method) { 41 | const nativeMethod = window.RTCPeerConnection.prototype[method]; 42 | const methodObj = {[method]() { 43 | arguments[0] = new ((method === 'addIceCandidate') ? 44 | window.RTCIceCandidate : 45 | window.RTCSessionDescription)(arguments[0]); 46 | return nativeMethod.apply(this, arguments); 47 | }}; 48 | window.RTCPeerConnection.prototype[method] = methodObj[method]; 49 | }); 50 | } 51 | 52 | const modernStatsTypes = { 53 | inboundrtp: 'inbound-rtp', 54 | outboundrtp: 'outbound-rtp', 55 | candidatepair: 'candidate-pair', 56 | localcandidate: 'local-candidate', 57 | remotecandidate: 'remote-candidate' 58 | }; 59 | 60 | const nativeGetStats = window.RTCPeerConnection.prototype.getStats; 61 | window.RTCPeerConnection.prototype.getStats = function getStats() { 62 | const [selector, onSucc, onErr] = arguments; 63 | return nativeGetStats.apply(this, [selector || null]) 64 | .then(stats => { 65 | if (browserDetails.version < 53 && !onSucc) { 66 | // Shim only promise getStats with spec-hyphens in type names 67 | // Leave callback version alone; misc old uses of forEach before Map 68 | try { 69 | stats.forEach(stat => { 70 | stat.type = modernStatsTypes[stat.type] || stat.type; 71 | }); 72 | } catch (e) { 73 | if (e.name !== 'TypeError') { 74 | throw e; 75 | } 76 | // Avoid TypeError: "type" is read-only, in old versions. 34-43ish 77 | stats.forEach((stat, i) => { 78 | stats.set(i, Object.assign({}, stat, { 79 | type: modernStatsTypes[stat.type] || stat.type 80 | })); 81 | }); 82 | } 83 | } 84 | return stats; 85 | }) 86 | .then(onSucc, onErr); 87 | }; 88 | } 89 | 90 | export function shimSenderGetStats(window) { 91 | if (!(typeof window === 'object' && window.RTCPeerConnection && 92 | window.RTCRtpSender)) { 93 | return; 94 | } 95 | if (window.RTCRtpSender && 'getStats' in window.RTCRtpSender.prototype) { 96 | return; 97 | } 98 | const origGetSenders = window.RTCPeerConnection.prototype.getSenders; 99 | if (origGetSenders) { 100 | window.RTCPeerConnection.prototype.getSenders = function getSenders() { 101 | const senders = origGetSenders.apply(this, []); 102 | senders.forEach(sender => sender._pc = this); 103 | return senders; 104 | }; 105 | } 106 | 107 | const origAddTrack = window.RTCPeerConnection.prototype.addTrack; 108 | if (origAddTrack) { 109 | window.RTCPeerConnection.prototype.addTrack = function addTrack() { 110 | const sender = origAddTrack.apply(this, arguments); 111 | sender._pc = this; 112 | return sender; 113 | }; 114 | } 115 | window.RTCRtpSender.prototype.getStats = function getStats() { 116 | return this.track ? this._pc.getStats(this.track) : 117 | Promise.resolve(new Map()); 118 | }; 119 | } 120 | 121 | export function shimReceiverGetStats(window) { 122 | if (!(typeof window === 'object' && window.RTCPeerConnection && 123 | window.RTCRtpSender)) { 124 | return; 125 | } 126 | if (window.RTCRtpSender && 'getStats' in window.RTCRtpReceiver.prototype) { 127 | return; 128 | } 129 | const origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; 130 | if (origGetReceivers) { 131 | window.RTCPeerConnection.prototype.getReceivers = function getReceivers() { 132 | const receivers = origGetReceivers.apply(this, []); 133 | receivers.forEach(receiver => receiver._pc = this); 134 | return receivers; 135 | }; 136 | } 137 | utils.wrapPeerConnectionEvent(window, 'track', e => { 138 | e.receiver._pc = e.srcElement; 139 | return e; 140 | }); 141 | window.RTCRtpReceiver.prototype.getStats = function getStats() { 142 | return this._pc.getStats(this.track); 143 | }; 144 | } 145 | 146 | export function shimRemoveStream(window) { 147 | if (!window.RTCPeerConnection || 148 | 'removeStream' in window.RTCPeerConnection.prototype) { 149 | return; 150 | } 151 | window.RTCPeerConnection.prototype.removeStream = 152 | function removeStream(stream) { 153 | utils.deprecated('removeStream', 'removeTrack'); 154 | this.getSenders().forEach(sender => { 155 | if (sender.track && stream.getTracks().includes(sender.track)) { 156 | this.removeTrack(sender); 157 | } 158 | }); 159 | }; 160 | } 161 | 162 | export function shimRTCDataChannel(window) { 163 | // rename DataChannel to RTCDataChannel (native fix in FF60): 164 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1173851 165 | if (window.DataChannel && !window.RTCDataChannel) { 166 | window.RTCDataChannel = window.DataChannel; 167 | } 168 | } 169 | 170 | export function shimAddTransceiver(window) { 171 | // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 172 | // Firefox ignores the init sendEncodings options passed to addTransceiver 173 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 174 | if (!(typeof window === 'object' && window.RTCPeerConnection)) { 175 | return; 176 | } 177 | const origAddTransceiver = window.RTCPeerConnection.prototype.addTransceiver; 178 | if (origAddTransceiver) { 179 | window.RTCPeerConnection.prototype.addTransceiver = 180 | function addTransceiver() { 181 | this.setParametersPromises = []; 182 | // WebIDL input coercion and validation 183 | let sendEncodings = arguments[1] && arguments[1].sendEncodings; 184 | if (sendEncodings === undefined) { 185 | sendEncodings = []; 186 | } 187 | sendEncodings = [...sendEncodings]; 188 | const shouldPerformCheck = sendEncodings.length > 0; 189 | if (shouldPerformCheck) { 190 | // If sendEncodings params are provided, validate grammar 191 | sendEncodings.forEach((encodingParam) => { 192 | if ('rid' in encodingParam) { 193 | const ridRegex = /^[a-z0-9]{0,16}$/i; 194 | if (!ridRegex.test(encodingParam.rid)) { 195 | throw new TypeError('Invalid RID value provided.'); 196 | } 197 | } 198 | if ('scaleResolutionDownBy' in encodingParam) { 199 | if (!(parseFloat(encodingParam.scaleResolutionDownBy) >= 1.0)) { 200 | throw new RangeError('scale_resolution_down_by must be >= 1.0'); 201 | } 202 | } 203 | if ('maxFramerate' in encodingParam) { 204 | if (!(parseFloat(encodingParam.maxFramerate) >= 0)) { 205 | throw new RangeError('max_framerate must be >= 0.0'); 206 | } 207 | } 208 | }); 209 | } 210 | const transceiver = origAddTransceiver.apply(this, arguments); 211 | if (shouldPerformCheck) { 212 | // Check if the init options were applied. If not we do this in an 213 | // asynchronous way and save the promise reference in a global object. 214 | // This is an ugly hack, but at the same time is way more robust than 215 | // checking the sender parameters before and after the createOffer 216 | // Also note that after the createoffer we are not 100% sure that 217 | // the params were asynchronously applied so we might miss the 218 | // opportunity to recreate offer. 219 | const {sender} = transceiver; 220 | const params = sender.getParameters(); 221 | if (!('encodings' in params) || 222 | // Avoid being fooled by patched getParameters() below. 223 | (params.encodings.length === 1 && 224 | Object.keys(params.encodings[0]).length === 0)) { 225 | params.encodings = sendEncodings; 226 | sender.sendEncodings = sendEncodings; 227 | this.setParametersPromises.push(sender.setParameters(params) 228 | .then(() => { 229 | delete sender.sendEncodings; 230 | }).catch(() => { 231 | delete sender.sendEncodings; 232 | }) 233 | ); 234 | } 235 | } 236 | return transceiver; 237 | }; 238 | } 239 | } 240 | 241 | export function shimGetParameters(window) { 242 | if (!(typeof window === 'object' && window.RTCRtpSender)) { 243 | return; 244 | } 245 | const origGetParameters = window.RTCRtpSender.prototype.getParameters; 246 | if (origGetParameters) { 247 | window.RTCRtpSender.prototype.getParameters = 248 | function getParameters() { 249 | const params = origGetParameters.apply(this, arguments); 250 | if (!('encodings' in params)) { 251 | params.encodings = [].concat(this.sendEncodings || [{}]); 252 | } 253 | return params; 254 | }; 255 | } 256 | } 257 | 258 | export function shimCreateOffer(window) { 259 | // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 260 | // Firefox ignores the init sendEncodings options passed to addTransceiver 261 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 262 | if (!(typeof window === 'object' && window.RTCPeerConnection)) { 263 | return; 264 | } 265 | const origCreateOffer = window.RTCPeerConnection.prototype.createOffer; 266 | window.RTCPeerConnection.prototype.createOffer = function createOffer() { 267 | if (this.setParametersPromises && this.setParametersPromises.length) { 268 | return Promise.all(this.setParametersPromises) 269 | .then(() => { 270 | return origCreateOffer.apply(this, arguments); 271 | }) 272 | .finally(() => { 273 | this.setParametersPromises = []; 274 | }); 275 | } 276 | return origCreateOffer.apply(this, arguments); 277 | }; 278 | } 279 | 280 | export function shimCreateAnswer(window) { 281 | // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 282 | // Firefox ignores the init sendEncodings options passed to addTransceiver 283 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 284 | if (!(typeof window === 'object' && window.RTCPeerConnection)) { 285 | return; 286 | } 287 | const origCreateAnswer = window.RTCPeerConnection.prototype.createAnswer; 288 | window.RTCPeerConnection.prototype.createAnswer = function createAnswer() { 289 | if (this.setParametersPromises && this.setParametersPromises.length) { 290 | return Promise.all(this.setParametersPromises) 291 | .then(() => { 292 | return origCreateAnswer.apply(this, arguments); 293 | }) 294 | .finally(() => { 295 | this.setParametersPromises = []; 296 | }); 297 | } 298 | return origCreateAnswer.apply(this, arguments); 299 | }; 300 | } 301 | -------------------------------------------------------------------------------- /src/js/firefox/getdisplaymedia.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 The adapter.js project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | export function shimGetDisplayMedia(window, preferredMediaSource) { 12 | if (window.navigator.mediaDevices && 13 | 'getDisplayMedia' in window.navigator.mediaDevices) { 14 | return; 15 | } 16 | if (!(window.navigator.mediaDevices)) { 17 | return; 18 | } 19 | window.navigator.mediaDevices.getDisplayMedia = 20 | function getDisplayMedia(constraints) { 21 | if (!(constraints && constraints.video)) { 22 | const err = new DOMException('getDisplayMedia without video ' + 23 | 'constraints is undefined'); 24 | err.name = 'NotFoundError'; 25 | // from https://heycam.github.io/webidl/#idl-DOMException-error-names 26 | err.code = 8; 27 | return Promise.reject(err); 28 | } 29 | if (constraints.video === true) { 30 | constraints.video = {mediaSource: preferredMediaSource}; 31 | } else { 32 | constraints.video.mediaSource = preferredMediaSource; 33 | } 34 | return window.navigator.mediaDevices.getUserMedia(constraints); 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/js/firefox/getusermedia.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | import * as utils from '../utils'; 12 | 13 | export function shimGetUserMedia(window, browserDetails) { 14 | const navigator = window && window.navigator; 15 | const MediaStreamTrack = window && window.MediaStreamTrack; 16 | 17 | navigator.getUserMedia = function(constraints, onSuccess, onError) { 18 | // Replace Firefox 44+'s deprecation warning with unprefixed version. 19 | utils.deprecated('navigator.getUserMedia', 20 | 'navigator.mediaDevices.getUserMedia'); 21 | navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError); 22 | }; 23 | 24 | if (!(browserDetails.version > 55 && 25 | 'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) { 26 | const remap = function(obj, a, b) { 27 | if (a in obj && !(b in obj)) { 28 | obj[b] = obj[a]; 29 | delete obj[a]; 30 | } 31 | }; 32 | 33 | const nativeGetUserMedia = navigator.mediaDevices.getUserMedia. 34 | bind(navigator.mediaDevices); 35 | navigator.mediaDevices.getUserMedia = function(c) { 36 | if (typeof c === 'object' && typeof c.audio === 'object') { 37 | c = JSON.parse(JSON.stringify(c)); 38 | remap(c.audio, 'autoGainControl', 'mozAutoGainControl'); 39 | remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression'); 40 | } 41 | return nativeGetUserMedia(c); 42 | }; 43 | 44 | if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) { 45 | const nativeGetSettings = MediaStreamTrack.prototype.getSettings; 46 | MediaStreamTrack.prototype.getSettings = function() { 47 | const obj = nativeGetSettings.apply(this, arguments); 48 | remap(obj, 'mozAutoGainControl', 'autoGainControl'); 49 | remap(obj, 'mozNoiseSuppression', 'noiseSuppression'); 50 | return obj; 51 | }; 52 | } 53 | 54 | if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) { 55 | const nativeApplyConstraints = 56 | MediaStreamTrack.prototype.applyConstraints; 57 | MediaStreamTrack.prototype.applyConstraints = function(c) { 58 | if (this.kind === 'audio' && typeof c === 'object') { 59 | c = JSON.parse(JSON.stringify(c)); 60 | remap(c, 'autoGainControl', 'mozAutoGainControl'); 61 | remap(c, 'noiseSuppression', 'mozNoiseSuppression'); 62 | } 63 | return nativeApplyConstraints.apply(this, [c]); 64 | }; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/js/safari/safari_shim.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 'use strict'; 9 | import * as utils from '../utils'; 10 | 11 | export function shimLocalStreamsAPI(window) { 12 | if (typeof window !== 'object' || !window.RTCPeerConnection) { 13 | return; 14 | } 15 | if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) { 16 | window.RTCPeerConnection.prototype.getLocalStreams = 17 | function getLocalStreams() { 18 | if (!this._localStreams) { 19 | this._localStreams = []; 20 | } 21 | return this._localStreams; 22 | }; 23 | } 24 | if (!('addStream' in window.RTCPeerConnection.prototype)) { 25 | const _addTrack = window.RTCPeerConnection.prototype.addTrack; 26 | window.RTCPeerConnection.prototype.addStream = function addStream(stream) { 27 | if (!this._localStreams) { 28 | this._localStreams = []; 29 | } 30 | if (!this._localStreams.includes(stream)) { 31 | this._localStreams.push(stream); 32 | } 33 | // Try to emulate Chrome's behaviour of adding in audio-video order. 34 | // Safari orders by track id. 35 | stream.getAudioTracks().forEach(track => _addTrack.call(this, track, 36 | stream)); 37 | stream.getVideoTracks().forEach(track => _addTrack.call(this, track, 38 | stream)); 39 | }; 40 | 41 | window.RTCPeerConnection.prototype.addTrack = 42 | function addTrack(track, ...streams) { 43 | if (streams) { 44 | streams.forEach((stream) => { 45 | if (!this._localStreams) { 46 | this._localStreams = [stream]; 47 | } else if (!this._localStreams.includes(stream)) { 48 | this._localStreams.push(stream); 49 | } 50 | }); 51 | } 52 | return _addTrack.apply(this, arguments); 53 | }; 54 | } 55 | if (!('removeStream' in window.RTCPeerConnection.prototype)) { 56 | window.RTCPeerConnection.prototype.removeStream = 57 | function removeStream(stream) { 58 | if (!this._localStreams) { 59 | this._localStreams = []; 60 | } 61 | const index = this._localStreams.indexOf(stream); 62 | if (index === -1) { 63 | return; 64 | } 65 | this._localStreams.splice(index, 1); 66 | const tracks = stream.getTracks(); 67 | this.getSenders().forEach(sender => { 68 | if (tracks.includes(sender.track)) { 69 | this.removeTrack(sender); 70 | } 71 | }); 72 | }; 73 | } 74 | } 75 | 76 | export function shimRemoteStreamsAPI(window) { 77 | if (typeof window !== 'object' || !window.RTCPeerConnection) { 78 | return; 79 | } 80 | if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) { 81 | window.RTCPeerConnection.prototype.getRemoteStreams = 82 | function getRemoteStreams() { 83 | return this._remoteStreams ? this._remoteStreams : []; 84 | }; 85 | } 86 | if (!('onaddstream' in window.RTCPeerConnection.prototype)) { 87 | Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', { 88 | get() { 89 | return this._onaddstream; 90 | }, 91 | set(f) { 92 | if (this._onaddstream) { 93 | this.removeEventListener('addstream', this._onaddstream); 94 | this.removeEventListener('track', this._onaddstreampoly); 95 | } 96 | this.addEventListener('addstream', this._onaddstream = f); 97 | this.addEventListener('track', this._onaddstreampoly = (e) => { 98 | e.streams.forEach(stream => { 99 | if (!this._remoteStreams) { 100 | this._remoteStreams = []; 101 | } 102 | if (this._remoteStreams.includes(stream)) { 103 | return; 104 | } 105 | this._remoteStreams.push(stream); 106 | const event = new Event('addstream'); 107 | event.stream = stream; 108 | this.dispatchEvent(event); 109 | }); 110 | }); 111 | } 112 | }); 113 | const origSetRemoteDescription = 114 | window.RTCPeerConnection.prototype.setRemoteDescription; 115 | window.RTCPeerConnection.prototype.setRemoteDescription = 116 | function setRemoteDescription() { 117 | const pc = this; 118 | if (!this._onaddstreampoly) { 119 | this.addEventListener('track', this._onaddstreampoly = function(e) { 120 | e.streams.forEach(stream => { 121 | if (!pc._remoteStreams) { 122 | pc._remoteStreams = []; 123 | } 124 | if (pc._remoteStreams.indexOf(stream) >= 0) { 125 | return; 126 | } 127 | pc._remoteStreams.push(stream); 128 | const event = new Event('addstream'); 129 | event.stream = stream; 130 | pc.dispatchEvent(event); 131 | }); 132 | }); 133 | } 134 | return origSetRemoteDescription.apply(pc, arguments); 135 | }; 136 | } 137 | } 138 | 139 | export function shimCallbacksAPI(window) { 140 | if (typeof window !== 'object' || !window.RTCPeerConnection) { 141 | return; 142 | } 143 | const prototype = window.RTCPeerConnection.prototype; 144 | const origCreateOffer = prototype.createOffer; 145 | const origCreateAnswer = prototype.createAnswer; 146 | const setLocalDescription = prototype.setLocalDescription; 147 | const setRemoteDescription = prototype.setRemoteDescription; 148 | const addIceCandidate = prototype.addIceCandidate; 149 | 150 | prototype.createOffer = 151 | function createOffer(successCallback, failureCallback) { 152 | const options = (arguments.length >= 2) ? arguments[2] : arguments[0]; 153 | const promise = origCreateOffer.apply(this, [options]); 154 | if (!failureCallback) { 155 | return promise; 156 | } 157 | promise.then(successCallback, failureCallback); 158 | return Promise.resolve(); 159 | }; 160 | 161 | prototype.createAnswer = 162 | function createAnswer(successCallback, failureCallback) { 163 | const options = (arguments.length >= 2) ? arguments[2] : arguments[0]; 164 | const promise = origCreateAnswer.apply(this, [options]); 165 | if (!failureCallback) { 166 | return promise; 167 | } 168 | promise.then(successCallback, failureCallback); 169 | return Promise.resolve(); 170 | }; 171 | 172 | let withCallback = function(description, successCallback, failureCallback) { 173 | const promise = setLocalDescription.apply(this, [description]); 174 | if (!failureCallback) { 175 | return promise; 176 | } 177 | promise.then(successCallback, failureCallback); 178 | return Promise.resolve(); 179 | }; 180 | prototype.setLocalDescription = withCallback; 181 | 182 | withCallback = function(description, successCallback, failureCallback) { 183 | const promise = setRemoteDescription.apply(this, [description]); 184 | if (!failureCallback) { 185 | return promise; 186 | } 187 | promise.then(successCallback, failureCallback); 188 | return Promise.resolve(); 189 | }; 190 | prototype.setRemoteDescription = withCallback; 191 | 192 | withCallback = function(candidate, successCallback, failureCallback) { 193 | const promise = addIceCandidate.apply(this, [candidate]); 194 | if (!failureCallback) { 195 | return promise; 196 | } 197 | promise.then(successCallback, failureCallback); 198 | return Promise.resolve(); 199 | }; 200 | prototype.addIceCandidate = withCallback; 201 | } 202 | 203 | export function shimGetUserMedia(window) { 204 | const navigator = window && window.navigator; 205 | 206 | if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 207 | // shim not needed in Safari 12.1 208 | const mediaDevices = navigator.mediaDevices; 209 | const _getUserMedia = mediaDevices.getUserMedia.bind(mediaDevices); 210 | navigator.mediaDevices.getUserMedia = (constraints) => { 211 | return _getUserMedia(shimConstraints(constraints)); 212 | }; 213 | } 214 | 215 | if (!navigator.getUserMedia && navigator.mediaDevices && 216 | navigator.mediaDevices.getUserMedia) { 217 | navigator.getUserMedia = function getUserMedia(constraints, cb, errcb) { 218 | navigator.mediaDevices.getUserMedia(constraints) 219 | .then(cb, errcb); 220 | }.bind(navigator); 221 | } 222 | } 223 | 224 | export function shimConstraints(constraints) { 225 | if (constraints && constraints.video !== undefined) { 226 | return Object.assign({}, 227 | constraints, 228 | {video: utils.compactObject(constraints.video)} 229 | ); 230 | } 231 | 232 | return constraints; 233 | } 234 | 235 | export function shimRTCIceServerUrls(window) { 236 | if (!window.RTCPeerConnection) { 237 | return; 238 | } 239 | // migrate from non-spec RTCIceServer.url to RTCIceServer.urls 240 | const OrigPeerConnection = window.RTCPeerConnection; 241 | window.RTCPeerConnection = 242 | function RTCPeerConnection(pcConfig, pcConstraints) { 243 | if (pcConfig && pcConfig.iceServers) { 244 | const newIceServers = []; 245 | for (let i = 0; i < pcConfig.iceServers.length; i++) { 246 | let server = pcConfig.iceServers[i]; 247 | if (server.urls === undefined && server.url) { 248 | utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls'); 249 | server = JSON.parse(JSON.stringify(server)); 250 | server.urls = server.url; 251 | delete server.url; 252 | newIceServers.push(server); 253 | } else { 254 | newIceServers.push(pcConfig.iceServers[i]); 255 | } 256 | } 257 | pcConfig.iceServers = newIceServers; 258 | } 259 | return new OrigPeerConnection(pcConfig, pcConstraints); 260 | }; 261 | window.RTCPeerConnection.prototype = OrigPeerConnection.prototype; 262 | // wrap static methods. Currently just generateCertificate. 263 | if ('generateCertificate' in OrigPeerConnection) { 264 | Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { 265 | get() { 266 | return OrigPeerConnection.generateCertificate; 267 | } 268 | }); 269 | } 270 | } 271 | 272 | export function shimTrackEventTransceiver(window) { 273 | // Add event.transceiver member over deprecated event.receiver 274 | if (typeof window === 'object' && window.RTCTrackEvent && 275 | 'receiver' in window.RTCTrackEvent.prototype && 276 | !('transceiver' in window.RTCTrackEvent.prototype)) { 277 | Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { 278 | get() { 279 | return {receiver: this.receiver}; 280 | } 281 | }); 282 | } 283 | } 284 | 285 | export function shimCreateOfferLegacy(window) { 286 | const origCreateOffer = window.RTCPeerConnection.prototype.createOffer; 287 | window.RTCPeerConnection.prototype.createOffer = 288 | function createOffer(offerOptions) { 289 | if (offerOptions) { 290 | if (typeof offerOptions.offerToReceiveAudio !== 'undefined') { 291 | // support bit values 292 | offerOptions.offerToReceiveAudio = 293 | !!offerOptions.offerToReceiveAudio; 294 | } 295 | const audioTransceiver = this.getTransceivers().find(transceiver => 296 | transceiver.receiver.track.kind === 'audio'); 297 | if (offerOptions.offerToReceiveAudio === false && audioTransceiver) { 298 | if (audioTransceiver.direction === 'sendrecv') { 299 | if (audioTransceiver.setDirection) { 300 | audioTransceiver.setDirection('sendonly'); 301 | } else { 302 | audioTransceiver.direction = 'sendonly'; 303 | } 304 | } else if (audioTransceiver.direction === 'recvonly') { 305 | if (audioTransceiver.setDirection) { 306 | audioTransceiver.setDirection('inactive'); 307 | } else { 308 | audioTransceiver.direction = 'inactive'; 309 | } 310 | } 311 | } else if (offerOptions.offerToReceiveAudio === true && 312 | !audioTransceiver) { 313 | this.addTransceiver('audio', {direction: 'recvonly'}); 314 | } 315 | 316 | if (typeof offerOptions.offerToReceiveVideo !== 'undefined') { 317 | // support bit values 318 | offerOptions.offerToReceiveVideo = 319 | !!offerOptions.offerToReceiveVideo; 320 | } 321 | const videoTransceiver = this.getTransceivers().find(transceiver => 322 | transceiver.receiver.track.kind === 'video'); 323 | if (offerOptions.offerToReceiveVideo === false && videoTransceiver) { 324 | if (videoTransceiver.direction === 'sendrecv') { 325 | if (videoTransceiver.setDirection) { 326 | videoTransceiver.setDirection('sendonly'); 327 | } else { 328 | videoTransceiver.direction = 'sendonly'; 329 | } 330 | } else if (videoTransceiver.direction === 'recvonly') { 331 | if (videoTransceiver.setDirection) { 332 | videoTransceiver.setDirection('inactive'); 333 | } else { 334 | videoTransceiver.direction = 'inactive'; 335 | } 336 | } 337 | } else if (offerOptions.offerToReceiveVideo === true && 338 | !videoTransceiver) { 339 | this.addTransceiver('video', {direction: 'recvonly'}); 340 | } 341 | } 342 | return origCreateOffer.apply(this, arguments); 343 | }; 344 | } 345 | 346 | export function shimAudioContext(window) { 347 | if (typeof window !== 'object' || window.AudioContext) { 348 | return; 349 | } 350 | window.AudioContext = window.webkitAudioContext; 351 | } 352 | 353 | -------------------------------------------------------------------------------- /src/js/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | let logDisabled_ = true; 12 | let deprecationWarnings_ = true; 13 | 14 | /** 15 | * Extract browser version out of the provided user agent string. 16 | * 17 | * @param {!string} uastring userAgent string. 18 | * @param {!string} expr Regular expression used as match criteria. 19 | * @param {!number} pos position in the version string to be returned. 20 | * @return {!number} browser version. 21 | */ 22 | export function extractVersion(uastring, expr, pos) { 23 | const match = uastring.match(expr); 24 | return match && match.length >= pos && parseInt(match[pos], 10); 25 | } 26 | 27 | // Wraps the peerconnection event eventNameToWrap in a function 28 | // which returns the modified event object (or false to prevent 29 | // the event). 30 | export function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) { 31 | if (!window.RTCPeerConnection) { 32 | return; 33 | } 34 | const proto = window.RTCPeerConnection.prototype; 35 | const nativeAddEventListener = proto.addEventListener; 36 | proto.addEventListener = function(nativeEventName, cb) { 37 | if (nativeEventName !== eventNameToWrap) { 38 | return nativeAddEventListener.apply(this, arguments); 39 | } 40 | const wrappedCallback = (e) => { 41 | const modifiedEvent = wrapper(e); 42 | if (modifiedEvent) { 43 | if (cb.handleEvent) { 44 | cb.handleEvent(modifiedEvent); 45 | } else { 46 | cb(modifiedEvent); 47 | } 48 | } 49 | }; 50 | this._eventMap = this._eventMap || {}; 51 | if (!this._eventMap[eventNameToWrap]) { 52 | this._eventMap[eventNameToWrap] = new Map(); 53 | } 54 | this._eventMap[eventNameToWrap].set(cb, wrappedCallback); 55 | return nativeAddEventListener.apply(this, [nativeEventName, 56 | wrappedCallback]); 57 | }; 58 | 59 | const nativeRemoveEventListener = proto.removeEventListener; 60 | proto.removeEventListener = function(nativeEventName, cb) { 61 | if (nativeEventName !== eventNameToWrap || !this._eventMap 62 | || !this._eventMap[eventNameToWrap]) { 63 | return nativeRemoveEventListener.apply(this, arguments); 64 | } 65 | if (!this._eventMap[eventNameToWrap].has(cb)) { 66 | return nativeRemoveEventListener.apply(this, arguments); 67 | } 68 | const unwrappedCb = this._eventMap[eventNameToWrap].get(cb); 69 | this._eventMap[eventNameToWrap].delete(cb); 70 | if (this._eventMap[eventNameToWrap].size === 0) { 71 | delete this._eventMap[eventNameToWrap]; 72 | } 73 | if (Object.keys(this._eventMap).length === 0) { 74 | delete this._eventMap; 75 | } 76 | return nativeRemoveEventListener.apply(this, [nativeEventName, 77 | unwrappedCb]); 78 | }; 79 | 80 | Object.defineProperty(proto, 'on' + eventNameToWrap, { 81 | get() { 82 | return this['_on' + eventNameToWrap]; 83 | }, 84 | set(cb) { 85 | if (this['_on' + eventNameToWrap]) { 86 | this.removeEventListener(eventNameToWrap, 87 | this['_on' + eventNameToWrap]); 88 | delete this['_on' + eventNameToWrap]; 89 | } 90 | if (cb) { 91 | this.addEventListener(eventNameToWrap, 92 | this['_on' + eventNameToWrap] = cb); 93 | } 94 | }, 95 | enumerable: true, 96 | configurable: true 97 | }); 98 | } 99 | 100 | export function disableLog(bool) { 101 | if (typeof bool !== 'boolean') { 102 | return new Error('Argument type: ' + typeof bool + 103 | '. Please use a boolean.'); 104 | } 105 | logDisabled_ = bool; 106 | return (bool) ? 'adapter.js logging disabled' : 107 | 'adapter.js logging enabled'; 108 | } 109 | 110 | /** 111 | * Disable or enable deprecation warnings 112 | * @param {!boolean} bool set to true to disable warnings. 113 | */ 114 | export function disableWarnings(bool) { 115 | if (typeof bool !== 'boolean') { 116 | return new Error('Argument type: ' + typeof bool + 117 | '. Please use a boolean.'); 118 | } 119 | deprecationWarnings_ = !bool; 120 | return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled'); 121 | } 122 | 123 | export function log() { 124 | if (typeof window === 'object') { 125 | if (logDisabled_) { 126 | return; 127 | } 128 | if (typeof console !== 'undefined' && typeof console.log === 'function') { 129 | console.log.apply(console, arguments); 130 | } 131 | } 132 | } 133 | 134 | /** 135 | * Shows a deprecation warning suggesting the modern and spec-compatible API. 136 | */ 137 | export function deprecated(oldMethod, newMethod) { 138 | if (!deprecationWarnings_) { 139 | return; 140 | } 141 | console.warn(oldMethod + ' is deprecated, please use ' + newMethod + 142 | ' instead.'); 143 | } 144 | 145 | /** 146 | * Browser detector. 147 | * 148 | * @return {object} result containing browser and version 149 | * properties. 150 | */ 151 | export function detectBrowser(window) { 152 | // Returned result object. 153 | const result = {browser: null, version: null}; 154 | 155 | // Fail early if it's not a browser 156 | if (typeof window === 'undefined' || !window.navigator || 157 | !window.navigator.userAgent) { 158 | result.browser = 'Not a browser.'; 159 | return result; 160 | } 161 | 162 | const {navigator} = window; 163 | 164 | // Prefer navigator.userAgentData. 165 | if (navigator.userAgentData && navigator.userAgentData.brands) { 166 | const chromium = navigator.userAgentData.brands.find((brand) => { 167 | return brand.brand === 'Chromium'; 168 | }); 169 | if (chromium) { 170 | return {browser: 'chrome', version: parseInt(chromium.version, 10)}; 171 | } 172 | } 173 | 174 | if (navigator.mozGetUserMedia) { // Firefox. 175 | result.browser = 'firefox'; 176 | result.version = extractVersion(navigator.userAgent, 177 | /Firefox\/(\d+)\./, 1); 178 | } else if (navigator.webkitGetUserMedia || 179 | (window.isSecureContext === false && window.webkitRTCPeerConnection)) { 180 | // Chrome, Chromium, Webview, Opera. 181 | // Version matches Chrome/WebRTC version. 182 | // Chrome 74 removed webkitGetUserMedia on http as well so we need the 183 | // more complicated fallback to webkitRTCPeerConnection. 184 | result.browser = 'chrome'; 185 | result.version = extractVersion(navigator.userAgent, 186 | /Chrom(e|ium)\/(\d+)\./, 2); 187 | } else if (window.RTCPeerConnection && 188 | navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { // Safari. 189 | result.browser = 'safari'; 190 | result.version = extractVersion(navigator.userAgent, 191 | /AppleWebKit\/(\d+)\./, 1); 192 | result.supportsUnifiedPlan = window.RTCRtpTransceiver && 193 | 'currentDirection' in window.RTCRtpTransceiver.prototype; 194 | } else { // Default fallthrough: not supported. 195 | result.browser = 'Not a supported browser.'; 196 | return result; 197 | } 198 | 199 | return result; 200 | } 201 | 202 | /** 203 | * Checks if something is an object. 204 | * 205 | * @param {*} val The something you want to check. 206 | * @return true if val is an object, false otherwise. 207 | */ 208 | function isObject(val) { 209 | return Object.prototype.toString.call(val) === '[object Object]'; 210 | } 211 | 212 | /** 213 | * Remove all empty objects and undefined values 214 | * from a nested object -- an enhanced and vanilla version 215 | * of Lodash's `compact`. 216 | */ 217 | export function compactObject(data) { 218 | if (!isObject(data)) { 219 | return data; 220 | } 221 | 222 | return Object.keys(data).reduce(function(accumulator, key) { 223 | const isObj = isObject(data[key]); 224 | const value = isObj ? compactObject(data[key]) : data[key]; 225 | const isEmptyObject = isObj && !Object.keys(value).length; 226 | if (value === undefined || isEmptyObject) { 227 | return accumulator; 228 | } 229 | return Object.assign(accumulator, {[key]: value}); 230 | }, {}); 231 | } 232 | 233 | /* iterates the stats graph recursively. */ 234 | export function walkStats(stats, base, resultSet) { 235 | if (!base || resultSet.has(base.id)) { 236 | return; 237 | } 238 | resultSet.set(base.id, base); 239 | Object.keys(base).forEach(name => { 240 | if (name.endsWith('Id')) { 241 | walkStats(stats, stats.get(base[name]), resultSet); 242 | } else if (name.endsWith('Ids')) { 243 | base[name].forEach(id => { 244 | walkStats(stats, stats.get(id), resultSet); 245 | }); 246 | } 247 | }); 248 | } 249 | 250 | /* filter getStats for a sender/receiver track. */ 251 | export function filterStats(result, track, outbound) { 252 | const streamStatsType = outbound ? 'outbound-rtp' : 'inbound-rtp'; 253 | const filteredResult = new Map(); 254 | if (track === null) { 255 | return filteredResult; 256 | } 257 | const trackStats = []; 258 | result.forEach(value => { 259 | if (value.type === 'track' && 260 | value.trackIdentifier === track.id) { 261 | trackStats.push(value); 262 | } 263 | }); 264 | trackStats.forEach(trackStat => { 265 | result.forEach(stats => { 266 | if (stats.type === streamStatsType && stats.trackId === trackStat.id) { 267 | walkStats(result, stats, filteredResult); 268 | } 269 | }); 270 | }); 271 | return filteredResult; 272 | } 273 | 274 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "rules": {}, 7 | "parserOptions": { 8 | "ecmaVersion": 2022 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/webrtc/samples.svg)](https://travis-ci.org/webrtc/samples) 2 | 3 | # Intro # 4 | 5 | Functional unit tests located in `test/unit` are run in node using [Mocha](https://mochajs.org/), [Chai](http://chaijs.com/) and [Sinon](http://sinonjs.org/). 6 | They are preferred way to test the behaviour of isolated pieces of code or when behaviour depends on the browser version. 7 | 8 | [Karma](http://karma-runner.github.io/1.0/index.html) is used to run the end-to-end tests which are also based on Mocha, Chai and Sinon. 9 | Those tests are run in many browsers using the different karma launchers for [Chrome](https://www.npmjs.com/package/karma-chrome-launcher), 10 | [Firefox](https://www.npmjs.com/package/karma-firefox-launcher), [MicrosoftEdge](https://www.npmjs.com/package/karma-edge-launcher) and 11 | [Safari](https://www.npmjs.com/package/karma-safari-launcher). Not all expected tests are expected to pass and they will be compared again 12 | expectation files similar to [Chrome tests](https://chromium.googlesource.com/chromium/src/+/lkcr/docs/testing/layout_test_expectations.md). 13 | This provides ensures stability while not restricting the project to tests that pass in all browsers. 14 | 15 | ## Development ## 16 | Detailed information on developing in the [webrtc](https://github.com/webrtc) GitHub repo can be mark in the [WebRTC GitHub repo developer's guide](https://docs.google.com/document/d/1tn1t6LW2ffzGuYTK3366w1fhTkkzsSvHsBnOHoDfRzY/edit?pli=1#heading=h.e3366rrgmkdk). 17 | 18 | This guide assumes you are running a Debian based Linux distribution (travis-multirunner currently fetches .deb browser packages). 19 | 20 | #### Clone the repo in desired folder 21 | ```bash 22 | git clone https://github.com/webrtc/adapter.git 23 | ``` 24 | 25 | #### Install npm dependencies 26 | ```bash 27 | npm install 28 | ``` 29 | 30 | #### Build 31 | In order to get a usable file, you need to build it. 32 | ```bash 33 | grunt build 34 | ``` 35 | This will result in 2 files in the out/ folder: 36 | * adapter.js - includes all the shims and is visible in the browser under the global `adapter` object (window.adapter). 37 | * adapter.js_no_global.js - same as adapter.js but is not exposed/visible in the browser (you cannot call/interact with the shims in the browser). 38 | 39 | #### Run tests 40 | Runs grunt and tests in test/tests.js. Change the browser to your choice, more details [here](#changeBrowser) 41 | ```bash 42 | BROWSER=chrome BVER=stable npm test 43 | ``` 44 | 45 | #### Add tests 46 | When adding tests make sure to update the test expectation file for all browsers and supported version. 47 | The easiest way to do so is to set the `CI` and `UPDATE_STABILITYREPORTER` environment variables and 48 | re-run the tests with all browsers. 49 | 50 | Once your test is ready, create a pull request and see how it runs on travis-multirunner. 51 | Usually the expectation is for a test to pass in at least one browser. File browser bugs 52 | for tests that do not meet this expectation! 53 | 54 | #### Change browser and channel/version for testing 55 | Chrome stable is currently installed as the default browser for the tests. 56 | 57 | Currently Chrome and Firefox are supported[*](#expBrowser), check [travis-multirunner](https://github.com/DamonOehlman/travis-multirunner/blob/master/) repo for updates around this. 58 | Firefox channels supported are stable, beta, nightly and ESR. 59 | Chrome channels supported on Linux are stable, beta and unstable. 60 | Microsoft Edge is supported on Windows and Safari on OSX. 61 | 62 | To select a different browser and/or channel version, change environment variables BROWSER and BVER, then you can rerun the tests with the new browser. 63 | ```bash 64 | export BROWSER=firefox BVER=nightly 65 | ``` 66 | 67 | Alternatively you can also do it without changing environment variables. 68 | ```bash 69 | BROWSER=firefox BVER=nightly npm test 70 | ``` 71 | 72 | ### Getting crash dumps from karma 73 | Sometimes Chrome may crash when running the tests. This typically shows up in headless runs as a disconnect: 74 | ``` 75 | 05 01 2018 10:42:14.225:WARN [HeadlessChrome 0.0.0 (Linux 0.0.0)]: Disconnected (1 times) 76 | ``` 77 | 78 | Follow these steps to get a crash dump: 79 | * add a `browsers = [];` line in test/karma.conf.js to stop karma from starting Chrome 80 | * change `singlerun` to `false` in test/karma.conf.js 81 | * run `node_modules/.bin/karma start test/karma.conf.js` in a terminal to start a karma server 82 | * start Chrome with `google-chrome --use-fake-device-for-media-stream --use-fake-ui-for-media-stream http://localhost:9876` 83 | * run `node_modules/.bin/karma run test/karma.conf.js` to start the karma run 84 | * wait for the "awww snap" :-) 85 | -------------------------------------------------------------------------------- /test/e2e/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true, 4 | "browser": true 5 | }, 6 | "rules": {}, 7 | "globals": { 8 | "expect": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/e2e/addIceCandidate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('addIceCandidate', () => { 12 | let pc; 13 | 14 | beforeEach(() => { 15 | const sdp = 'v=0\r\n' + 16 | 'o=- 166855176514521964 2 IN IP4 127.0.0.1\r\n' + 17 | 's=-\r\n' + 18 | 't=0 0\r\n' + 19 | 'a=msid-semantic:WMS *\r\n' + 20 | 'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' + 21 | 'c=IN IP4 0.0.0.0\r\n' + 22 | 'a=rtcp:9 IN IP4 0.0.0.0\r\n' + 23 | 'a=ice-ufrag:someufrag\r\n' + 24 | 'a=ice-pwd:somelongpwdwithenoughrandomness\r\n' + 25 | 'a=fingerprint:sha-256 8C:71:B3:8D:A5:38:FD:8F:A4:2E:A2:65:6C:86:52' + 26 | ':BC:E0:6E:94:F2:9F:7C:4D:B5:DF:AF:AA:6F:44:90:8D:F4\r\n' + 27 | 'a=setup:actpass\r\n' + 28 | 'a=rtcp-mux\r\n' + 29 | 'a=mid:mid1\r\n' + 30 | 'a=sendonly\r\n' + 31 | 'a=rtpmap:111 opus/48000/2\r\n' + 32 | 'a=msid:stream1 track1\r\n' + 33 | 'a=ssrc:1001 cname:some\r\n'; 34 | pc = new RTCPeerConnection(); 35 | return pc.setRemoteDescription({type: 'offer', sdp}) 36 | .then(() => { 37 | return pc.addIceCandidate({sdpMid: 'mid1', candidate: 38 | 'candidate:702786350 1 udp 41819902 8.8.8.8 60769 typ host'}); 39 | }); 40 | }); 41 | afterEach(() => { 42 | pc.close(); 43 | }); 44 | 45 | describe('after setRemoteDescription', () => { 46 | it('resolves when called with null', () => 47 | pc.addIceCandidate(null) 48 | ); 49 | 50 | it('resolves when called with undefined', () => 51 | pc.addIceCandidate(undefined) 52 | ); 53 | 54 | it('resolves when called with {candidate: \'\'}', () => 55 | pc.addIceCandidate({candidate: '', sdpMid: 'mid1'}) 56 | ); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/e2e/addTrack.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('addTrack', () => { 12 | let pc; 13 | beforeEach(() => { 14 | pc = new RTCPeerConnection(); 15 | }); 16 | afterEach(() => { 17 | if (pc.signalingState !== 'closed') { 18 | pc.close(); 19 | } 20 | }); 21 | 22 | describe('throws an exception', () => { 23 | it('if the track has already been added', () => { 24 | return navigator.mediaDevices.getUserMedia({audio: true}) 25 | .then(stream => { 26 | pc.addTrack(stream.getTracks()[0], stream); 27 | const again = () => { 28 | pc.addTrack(stream.getTracks()[0], stream); 29 | }; 30 | expect(again).to.throw(/already/) 31 | .that.has.property('name').that.equals('InvalidAccessError'); 32 | }); 33 | }); 34 | 35 | it('if the track has already been added via addStream', () => { 36 | return navigator.mediaDevices.getUserMedia({audio: true}) 37 | .then(stream => { 38 | pc.addStream(stream); 39 | const again = () => { 40 | pc.addTrack(stream.getTracks()[0], stream); 41 | }; 42 | expect(again).to.throw(/already/) 43 | .that.has.property('name').that.equals('InvalidAccessError'); 44 | }); 45 | }); 46 | 47 | it('if addStream is called with a stream containing a track ' + 48 | 'already added', () => { 49 | return navigator.mediaDevices.getUserMedia({audio: true, video: true}) 50 | .then(stream => { 51 | pc.addTrack(stream.getTracks()[0], stream); 52 | const again = () => { 53 | pc.addStream(stream); 54 | }; 55 | expect(again).to.throw(/already/) 56 | .that.has.property('name').that.equals('InvalidAccessError'); 57 | }); 58 | }); 59 | 60 | it('if the peerconnection has been closed already', () => { 61 | return navigator.mediaDevices.getUserMedia({audio: true}) 62 | .then(stream => { 63 | pc.close(); 64 | const afterClose = () => { 65 | pc.addTrack(stream.getTracks()[0], stream); 66 | }; 67 | expect(afterClose).to.throw(/closed/) 68 | .that.has.property('name').that.equals('InvalidStateError'); 69 | }); 70 | }); 71 | }); 72 | 73 | describe('and getSenders', () => { 74 | it('creates a sender', () => { 75 | return navigator.mediaDevices.getUserMedia({audio: true}) 76 | .then(stream => { 77 | pc.addTrack(stream.getTracks()[0], stream); 78 | const senders = pc.getSenders(); 79 | expect(senders).to.have.length(1); 80 | expect(senders[0].track).to.equal(stream.getTracks()[0]); 81 | }); 82 | }); 83 | }); 84 | 85 | describe('and getLocalStreams', () => { 86 | it('returns a stream with audio and video even if just an ' + 87 | 'audio track was added', () => { 88 | return navigator.mediaDevices.getUserMedia({audio: true, video: true}) 89 | .then(stream => { 90 | pc.addTrack(stream.getTracks()[0], stream); 91 | const localStreams = pc.getLocalStreams(); 92 | expect(localStreams).to.have.length(1); 93 | expect(localStreams[0].getTracks()).to.have.length(2); 94 | expect(pc.getSenders()).to.have.length(1); 95 | }); 96 | }); 97 | 98 | it('adds another track to the same stream', () => { 99 | return navigator.mediaDevices.getUserMedia({audio: true, video: true}) 100 | .then(stream => { 101 | pc.addTrack(stream.getTracks()[0], stream); 102 | const localStreams = pc.getLocalStreams(); 103 | expect(localStreams).to.have.length(1); 104 | expect(localStreams[0].getTracks()).to.have.length(2); 105 | expect(pc.getSenders()).to.have.length(1); 106 | 107 | pc.addTrack(stream.getTracks()[1], stream); 108 | expect(pc.getLocalStreams()).to.have.length(1); 109 | expect(pc.getSenders()).to.have.length(2); 110 | }); 111 | }); 112 | 113 | it('plays together nicely', () => { 114 | return navigator.mediaDevices.getUserMedia({audio: true}) 115 | .then(stream => { 116 | pc.addTrack(stream.getTracks()[0], stream); 117 | const localStreams = pc.getLocalStreams(); 118 | expect(localStreams).to.have.length(1); 119 | expect(localStreams[0].getTracks()).to.have.length(1); 120 | expect(pc.getSenders()).to.have.length(1); 121 | return navigator.mediaDevices.getUserMedia({video: true}); 122 | }) 123 | .then(stream => { 124 | const localStreams = pc.getLocalStreams(); 125 | const localStream = localStreams[0]; 126 | const track = stream.getTracks()[0]; 127 | localStream.addTrack(track); 128 | pc.addTrack(track, localStream); 129 | expect(localStreams).to.have.length(1); 130 | expect(localStreams[0].getTracks()).to.have.length(2); 131 | expect(pc.getSenders()).to.have.length(2); 132 | }); 133 | }); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /test/e2e/browserdetails.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('window.adapter', () => { 12 | it('exists', () => { 13 | expect(window).to.have.property('adapter'); 14 | }); 15 | 16 | describe('browserDetails', () => { 17 | it('exists', () => { 18 | expect(window.adapter).to.have.property('browserDetails'); 19 | }); 20 | 21 | it('detects a browser type', () => { 22 | expect(window.adapter.browserDetails).to.have.property('browser'); 23 | }); 24 | 25 | it('detects a browser version', () => { 26 | expect(window.adapter.browserDetails).to.have.property('version'); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/e2e/connection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('establishes a connection', () => { 12 | let pc1; 13 | let pc2; 14 | function noop() {} 15 | function throwError(err) { 16 | console.error(err.toString()); 17 | throw err; 18 | } 19 | 20 | function negotiate(pc, otherPc) { 21 | return pc.createOffer() 22 | .then(function(offer) { 23 | return pc.setLocalDescription(offer); 24 | }).then(function() { 25 | return otherPc.setRemoteDescription(pc.localDescription); 26 | }).then(function() { 27 | return otherPc.createAnswer(); 28 | }).then(function(answer) { 29 | return otherPc.setLocalDescription(answer); 30 | }).then(function() { 31 | return pc.setRemoteDescription(otherPc.localDescription); 32 | }); 33 | } 34 | 35 | beforeEach(() => { 36 | pc1 = new RTCPeerConnection(null); 37 | pc2 = new RTCPeerConnection(null); 38 | 39 | pc1.onicecandidate = event => pc2.addIceCandidate(event.candidate); 40 | pc2.onicecandidate = event => pc1.addIceCandidate(event.candidate); 41 | }); 42 | afterEach(() => { 43 | pc1.close(); 44 | pc2.close(); 45 | }); 46 | 47 | it('with legacy callbacks', (done) => { 48 | pc1.onicecandidate = function(event) { 49 | pc2.addIceCandidate(event.candidate, noop, throwError); 50 | }; 51 | pc2.onicecandidate = function(event) { 52 | pc1.addIceCandidate(event.candidate, noop, throwError); 53 | }; 54 | pc1.oniceconnectionstatechange = function() { 55 | if (pc1.iceConnectionState === 'connected' || 56 | pc1.iceConnectionState === 'completed') { 57 | done(); 58 | } 59 | }; 60 | 61 | var constraints = {video: true}; 62 | navigator.mediaDevices.getUserMedia(constraints) 63 | .then(function(stream) { 64 | pc1.addStream(stream); 65 | 66 | pc1.createOffer( 67 | function(offer) { 68 | pc1.setLocalDescription(offer, 69 | function() { 70 | pc2.setRemoteDescription(offer, 71 | function() { 72 | pc2.createAnswer( 73 | function(answer) { 74 | pc2.setLocalDescription(answer, 75 | function() { 76 | pc1.setRemoteDescription(answer, noop, throwError); 77 | }, 78 | throwError 79 | ); 80 | }, 81 | throwError 82 | ); 83 | }, 84 | throwError 85 | ); 86 | }, 87 | throwError 88 | ); 89 | }, 90 | throwError 91 | ); 92 | }); 93 | }); 94 | 95 | it('with promises', (done) => { 96 | pc1.oniceconnectionstatechange = function() { 97 | if (pc1.iceConnectionState === 'connected' || 98 | pc1.iceConnectionState === 'completed') { 99 | done(); 100 | } 101 | }; 102 | 103 | var constraints = {video: true}; 104 | navigator.mediaDevices.getUserMedia(constraints) 105 | .then(function(stream) { 106 | pc1.addStream(stream); 107 | return negotiate(pc1, pc2); 108 | }) 109 | .catch(throwError); 110 | }); 111 | 112 | it('with streams in both directions', (done) => { 113 | pc1.oniceconnectionstatechange = function() { 114 | if (pc1.iceConnectionState === 'connected' || 115 | pc1.iceConnectionState === 'completed') { 116 | done(); 117 | } 118 | }; 119 | 120 | var constraints = {video: true}; 121 | navigator.mediaDevices.getUserMedia(constraints) 122 | .then(function(stream) { 123 | pc1.addStream(stream); 124 | pc2.addStream(stream); 125 | return negotiate(pc1, pc2); 126 | }) 127 | .catch(throwError); 128 | }); 129 | 130 | describe('with addTrack', () => { 131 | it('and all tracks of a stream', (done) => { 132 | pc1.oniceconnectionstatechange = function() { 133 | if (pc1.iceConnectionState === 'connected' || 134 | pc1.iceConnectionState === 'completed') { 135 | done(); 136 | } 137 | }; 138 | 139 | pc2.onaddstream = function(event) { 140 | expect(event).to.have.property('stream'); 141 | expect(event.stream.getAudioTracks()).to.have.length(1); 142 | expect(event.stream.getVideoTracks()).to.have.length(1); 143 | }; 144 | 145 | var constraints = {audio: true, video: true}; 146 | navigator.mediaDevices.getUserMedia(constraints) 147 | .then(function(stream) { 148 | stream.getTracks().forEach(function(track) { 149 | pc1.addTrack(track, stream); 150 | }); 151 | return negotiate(pc1, pc2); 152 | }) 153 | .catch(throwError); 154 | }); 155 | 156 | it('but only the audio track of an av stream', (done) => { 157 | pc1.oniceconnectionstatechange = function() { 158 | if (pc1.iceConnectionState === 'connected' || 159 | pc1.iceConnectionState === 'completed') { 160 | done(); 161 | } 162 | }; 163 | 164 | pc2.onaddstream = function(event) { 165 | expect(event).to.have.property('stream'); 166 | expect(event.stream.getAudioTracks()).to.have.length(1); 167 | expect(event.stream.getVideoTracks()).to.have.length(0); 168 | }; 169 | 170 | var constraints = {audio: true, video: true}; 171 | navigator.mediaDevices.getUserMedia(constraints) 172 | .then(function(stream) { 173 | stream.getAudioTracks().forEach(function(track) { 174 | pc1.addTrack(track, stream); 175 | }); 176 | return negotiate(pc1, pc2); 177 | }) 178 | .catch(throwError); 179 | }); 180 | 181 | it('as two streams', (done) => { 182 | let streams = []; 183 | pc1.oniceconnectionstatechange = function() { 184 | if (pc1.iceConnectionState === 'connected' || 185 | pc1.iceConnectionState === 'completed') { 186 | expect(streams).to.have.length(2); 187 | done(); 188 | } 189 | }; 190 | 191 | pc2.onaddstream = function(event) { 192 | expect(event).to.have.property('stream'); 193 | expect(event.stream.getTracks()).to.have.length(1); 194 | streams.push(event.stream); 195 | }; 196 | 197 | var constraints = {audio: true, video: true}; 198 | navigator.mediaDevices.getUserMedia(constraints) 199 | .then(function(stream) { 200 | var audioStream = new MediaStream(stream.getAudioTracks()); 201 | var videoStream = new MediaStream(stream.getVideoTracks()); 202 | audioStream.getTracks().forEach(function(track) { 203 | pc1.addTrack(track, audioStream); 204 | }); 205 | videoStream.getTracks().forEach(function(track) { 206 | pc1.addTrack(track, videoStream); 207 | }); 208 | return negotiate(pc1, pc2); 209 | }) 210 | .catch(throwError); 211 | }); 212 | }); 213 | 214 | it('with no explicit end-of-candidates', function(done) { 215 | pc1.oniceconnectionstatechange = function() { 216 | if (pc1.iceConnectionState === 'connected' || 217 | pc1.iceConnectionState === 'completed') { 218 | done(); 219 | } 220 | }; 221 | 222 | pc1.onicecandidate = (event) => { 223 | if (event.candidate) { 224 | pc2.addIceCandidate(event.candidate, noop, throwError); 225 | } 226 | }; 227 | pc2.onicecandidate = (event) => { 228 | if (event.candidate) { 229 | pc1.addIceCandidate(event.candidate, noop, throwError); 230 | } 231 | }; 232 | 233 | var constraints = {video: true}; 234 | navigator.mediaDevices.getUserMedia(constraints) 235 | .then(function(stream) { 236 | stream.getTracks().forEach(function(track) { 237 | pc1.addTrack(track, stream); 238 | }); 239 | return negotiate(pc1, pc2); 240 | }) 241 | .catch(throwError); 242 | }); 243 | 244 | describe('with datachannel', function() { 245 | it('establishes a connection', (done) => { 246 | pc1.oniceconnectionstatechange = function() { 247 | if (pc1.iceConnectionState === 'connected' || 248 | pc1.iceConnectionState === 'completed') { 249 | done(); 250 | } 251 | }; 252 | 253 | pc1.createDataChannel('foo'); 254 | negotiate(pc1, pc2) 255 | .catch(throwError); 256 | }); 257 | }); 258 | 259 | it('and calls the video loadedmetadata', (done) => { 260 | pc2.addEventListener('addstream', function(e) { 261 | var v = document.createElement('video'); 262 | v.autoplay = true; 263 | v.addEventListener('loadedmetadata', function() { 264 | done(); 265 | }); 266 | v.srcObject = e.stream; 267 | }); 268 | var constraints = {video: true}; 269 | navigator.mediaDevices.getUserMedia(constraints) 270 | .then(function(stream) { 271 | stream.getTracks().forEach(function(track) { 272 | pc1.addTrack(track, stream); 273 | }); 274 | return negotiate(pc1, pc2); 275 | }) 276 | .catch(throwError); 277 | }); 278 | 279 | it('and triggers the connectionstatechange event', (done) => { 280 | pc1.onconnectionstatechange = function() { 281 | if (pc1.connectionState === 'connected') { 282 | done(); 283 | } 284 | }; 285 | 286 | var constraints = {video: true}; 287 | navigator.mediaDevices.getUserMedia(constraints) 288 | .then(function(stream) { 289 | pc1.addStream(stream); 290 | return negotiate(pc1, pc2); 291 | }) 292 | .catch(throwError); 293 | }); 294 | }); 295 | -------------------------------------------------------------------------------- /test/e2e/dtmf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | 12 | describe('dtmf', () => { 13 | describe('RTCRtpSender.dtmf', () => { 14 | // we can not test existence on the prototype because we do 15 | // not shim RTCRtpSender when it does not exist. 16 | it('exists on audio senders', () => { 17 | const pc = new RTCPeerConnection(); 18 | return navigator.mediaDevices.getUserMedia({audio: true}) 19 | .then(stream => { 20 | pc.addStream(stream); 21 | const senders = pc.getSenders(); 22 | const dtmf = senders[0].dtmf; 23 | expect(dtmf).not.to.equal(null); 24 | expect(dtmf).to.have.property('insertDTMF'); 25 | }); 26 | }); 27 | 28 | it('does not exist on video senders', () => { 29 | const pc = new RTCPeerConnection(); 30 | return navigator.mediaDevices.getUserMedia({video: true}) 31 | .then(stream => { 32 | pc.addStream(stream); 33 | const senders = pc.getSenders(); 34 | const dtmf = senders[0].dtmf; 35 | expect(dtmf).to.equal(null); 36 | }); 37 | }); 38 | }); 39 | 40 | describe('inserts DTMF', () => { 41 | let pc1; 42 | let pc2; 43 | 44 | beforeEach(() => { 45 | pc1 = new RTCPeerConnection(null); 46 | pc2 = new RTCPeerConnection(null); 47 | 48 | pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 49 | pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); 50 | pc1.onnegotiationneeded = e => pc1.createOffer() 51 | .then(offer => pc1.setLocalDescription(offer)) 52 | .then(() => pc2.setRemoteDescription(pc1.localDescription)) 53 | .then(() => pc2.createAnswer()) 54 | .then(answer => pc2.setLocalDescription(answer)) 55 | .then(() => pc1.setRemoteDescription(pc2.localDescription)); 56 | }); 57 | afterEach(() => { 58 | pc1.close(); 59 | pc2.close(); 60 | }); 61 | 62 | it('when using addStream', () => { 63 | return navigator.mediaDevices.getUserMedia({audio: true}) 64 | .then(stream => pc1.addStream(stream)) 65 | .then(() => { 66 | return pc1.iceConnectionState === 'connected' || 67 | pc1.iceConnectionState === 'completed' || 68 | new Promise(resolve => pc1.oniceconnectionstatechange = 69 | e => (pc1.iceConnectionState === 'connected' || 70 | pc1.iceConnectionState === 'completed') && resolve()); 71 | }) 72 | .then(() => { 73 | return pc2.iceConnectionState === 'connected' || 74 | pc2.iceConnectionState === 'completed' || 75 | new Promise(resolve => pc2.oniceconnectionstatechange = 76 | e => (pc2.iceConnectionState === 'connected' || 77 | pc2.iceConnectionState === 'completed') && resolve()); 78 | }) 79 | .then(() => { 80 | if (!(window.RTCDTMFSender && 81 | 'canInsertDTMF' in window.RTCDTMFSender.prototype)) { 82 | return; 83 | } 84 | return new Promise((resolve) => { 85 | setTimeout(function canInsert() { 86 | const sender = pc1.getSenders() 87 | .find(s => s.track.kind === 'audio'); 88 | if (sender.dtmf.canInsertDTMF) { 89 | return resolve(); 90 | } 91 | setTimeout(canInsert, 10); 92 | }, 0); 93 | }); 94 | }) 95 | .then(() => { 96 | const sender = pc1.getSenders().find(s => s.track.kind === 'audio'); 97 | sender.dtmf.insertDTMF('1'); 98 | return new Promise(resolve => sender.dtmf.ontonechange = resolve); 99 | }) 100 | .then(toneEvent => { 101 | expect(toneEvent.tone).to.equal('1'); 102 | }); 103 | }); 104 | 105 | it('when using addTrack', () => { 106 | return navigator.mediaDevices.getUserMedia({audio: true}) 107 | .then(stream => pc1.addTrack(stream.getAudioTracks()[0], stream)) 108 | .then(() => { 109 | return pc1.iceConnectionState === 'connected' || 110 | pc1.iceConnectionState === 'completed' || 111 | new Promise(resolve => pc1.oniceconnectionstatechange = 112 | e => (pc1.iceConnectionState === 'connected' || 113 | pc1.iceConnectionState === 'completed') && resolve()); 114 | }) 115 | .then(() => { 116 | return pc2.iceConnectionState === 'connected' || 117 | pc2.iceConnectionState === 'completed' || 118 | new Promise(resolve => pc2.oniceconnectionstatechange = 119 | e => (pc2.iceConnectionState === 'connected' || 120 | pc2.iceConnectionState === 'completed') && resolve()); 121 | }) 122 | .then(() => { 123 | if (!(window.RTCDTMFSender && 124 | 'canInsertDTMF' in window.RTCDTMFSender.prototype)) { 125 | return; 126 | } 127 | return new Promise((resolve) => { 128 | setTimeout(function canInsert() { 129 | const sender = pc1.getSenders() 130 | .find(s => s.track.kind === 'audio'); 131 | if (sender.dtmf.canInsertDTMF) { 132 | return resolve(); 133 | } 134 | setTimeout(canInsert, 10); 135 | }, 0); 136 | }); 137 | }) 138 | .then(() => { 139 | const sender = pc1.getSenders().find(s => s.track.kind === 'audio'); 140 | sender.dtmf.insertDTMF('1'); 141 | return new Promise(resolve => sender.dtmf.ontonechange = resolve); 142 | }) 143 | .then(toneEvent => { 144 | expect(toneEvent.tone).to.equal('1'); 145 | }); 146 | }); 147 | }).timeout(5000); 148 | }); 149 | -------------------------------------------------------------------------------- /test/e2e/expectations/MicrosoftEdge: -------------------------------------------------------------------------------- 1 | ERR addTrack throws an exception if the track has already been added AssertionError 2 | ERR addTrack throws an exception if the track has already been added via addStream AssertionError 3 | ERR addTrack throws an exception if addStream is called with a stream containing a track already added AssertionError 4 | ERR addTrack throws an exception if the peerconnection has been closed already AssertionError 5 | SKIP establishes a connection with datachannel establishes a connection 6 | ERR track event RTCPeerConnection.prototype.ontrack exists AssertionError 7 | ERR removeTrack allows removeTrack twice timeout 8 | ERR removeTrack throws an exception if the argument is a track, not a sender AssertionError 9 | ERR removeTrack throws an exception if the sender does not belong to the peerconnection AssertionError 10 | ERR removeTrack throws an exception if the peerconnection has been closed already AssertionError 11 | ERR removeTrack after addStream for an audio/video track after removing a single track only a single sender with a track remains timeout 12 | ERR removeTrack after addStream for an audio/video track after removing a single track the local stream remains untouched timeout 13 | ERR removeTrack after addStream for an audio/video track after removing all tracks no senders with tracks remain timeout 14 | ERR removeTrack after addStream for an audio/video track after removing all tracks no local streams remain timeout 15 | ERR removeTrack after addTrack for an audio/video track after removing a single track only a single sender with a track remains timeout 16 | ERR removeTrack after addTrack for an audio/video track after removing a single track the local stream remains untouched timeout 17 | ERR removeTrack after addTrack for an audio/video track after removing all tracks no senders with tracks remain timeout 18 | ERR removeTrack after addTrack for an audio/video track after removing all tracks no local streams remain timeout 19 | ERR RTCPeerConnection generateCertificate is a static method AssertionError 20 | -------------------------------------------------------------------------------- /test/e2e/expectations/chrome-beta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrtc/adapter/b5e5f710b85b9505e5633066cbd15b89f58b7562/test/e2e/expectations/chrome-beta -------------------------------------------------------------------------------- /test/e2e/expectations/chrome-beta-no-experimental: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrtc/adapter/b5e5f710b85b9505e5633066cbd15b89f58b7562/test/e2e/expectations/chrome-beta-no-experimental -------------------------------------------------------------------------------- /test/e2e/expectations/chrome-dev: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrtc/adapter/b5e5f710b85b9505e5633066cbd15b89f58b7562/test/e2e/expectations/chrome-dev -------------------------------------------------------------------------------- /test/e2e/expectations/chrome-stable: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrtc/adapter/b5e5f710b85b9505e5633066cbd15b89f58b7562/test/e2e/expectations/chrome-stable -------------------------------------------------------------------------------- /test/e2e/expectations/chrome-stable-no-experimental: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrtc/adapter/b5e5f710b85b9505e5633066cbd15b89f58b7562/test/e2e/expectations/chrome-stable-no-experimental -------------------------------------------------------------------------------- /test/e2e/expectations/firefox-beta: -------------------------------------------------------------------------------- 1 | ERR navigator.mediaDevices enumerateDevices returns some audiooutput devices AssertionError 2 | ERR removeTrack after addStream for an audio/video track after removing all tracks no local streams remain AssertionError 3 | ERR removeTrack after addTrack for an audio/video track after removing all tracks no local streams remain AssertionError 4 | -------------------------------------------------------------------------------- /test/e2e/expectations/firefox-esr: -------------------------------------------------------------------------------- 1 | ERR navigator.mediaDevices enumerateDevices returns some audiooutput devices AssertionError 2 | ERR removeTrack after addStream for an audio/video track after removing all tracks no local streams remain AssertionError 3 | ERR removeTrack after addTrack for an audio/video track after removing all tracks no local streams remain AssertionError 4 | -------------------------------------------------------------------------------- /test/e2e/expectations/firefox-nightly: -------------------------------------------------------------------------------- 1 | ERR navigator.mediaDevices enumerateDevices returns some audiooutput devices AssertionError 2 | ERR removeTrack after addStream for an audio/video track after removing all tracks no local streams remain AssertionError 3 | ERR removeTrack after addTrack for an audio/video track after removing all tracks no local streams remain AssertionError 4 | -------------------------------------------------------------------------------- /test/e2e/expectations/firefox-stable: -------------------------------------------------------------------------------- 1 | ERR navigator.mediaDevices enumerateDevices returns some audiooutput devices AssertionError 2 | ERR removeTrack after addStream for an audio/video track after removing all tracks no local streams remain AssertionError 3 | ERR removeTrack after addTrack for an audio/video track after removing all tracks no local streams remain AssertionError 4 | -------------------------------------------------------------------------------- /test/e2e/getStats.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('getStats', () => { 12 | let pc; 13 | beforeEach(() => { 14 | pc = new RTCPeerConnection(); 15 | }); 16 | afterEach(() => { 17 | pc.close(); 18 | }); 19 | 20 | it('returns a Promise', () => { 21 | return pc.getStats(); 22 | }); 23 | 24 | it('resolves the Promise with a Map(like)', () => { 25 | return pc.getStats() 26 | .then(result => { 27 | expect(result).to.have.property('get'); 28 | expect(result).to.have.property('keys'); 29 | expect(result).to.have.property('values'); 30 | expect(result).to.have.property('forEach'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/e2e/getusermedia.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('getUserMedia', () => { 12 | describe('navigator.getUserMedia', () => { 13 | it('exists', () => { 14 | expect(navigator).to.have.property('getUserMedia'); 15 | }); 16 | 17 | it('calls the callback', (done) => { 18 | navigator.getUserMedia({video: true}, (stream) => { 19 | expect(stream.getTracks()).to.have.length(1); 20 | done(); 21 | }, (err) => { 22 | throw err; 23 | }); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/e2e/mediaDevices.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('navigator.mediaDevices', () => { 12 | it('exists', () => { 13 | expect(navigator).to.have.property('mediaDevices'); 14 | }); 15 | 16 | describe('getUserMedia', () => { 17 | it('exists', () => { 18 | expect(navigator.mediaDevices).to.have.property('getUserMedia'); 19 | }); 20 | 21 | it('fulfills the promise', () => { 22 | return navigator.mediaDevices.getUserMedia({video: true}) 23 | .then((stream) => { 24 | expect(stream.getTracks()).to.have.length(1); 25 | }); 26 | }); 27 | }); 28 | 29 | it('is an EventTarget', () => { 30 | // Test that adding and removing an eventlistener on navigator.mediaDevices 31 | // is possible. The usecase for this is the devicechanged event. 32 | // This does not test whether devicechanged is actually called. 33 | expect(navigator.mediaDevices).to.have.property('addEventListener'); 34 | expect(navigator.mediaDevices).to.have.property('removeEventListener'); 35 | }); 36 | 37 | it('implements the devicechange event', () => { 38 | expect(navigator.mediaDevices).to.have.property('ondevicechange'); 39 | }); 40 | 41 | describe('enumerateDevices', () => { 42 | it('exists', () => { 43 | expect(navigator.mediaDevices).to.have.property('enumerateDevices'); 44 | }); 45 | 46 | describe('returns', () => { 47 | it('an array of devices', () => { 48 | return navigator.mediaDevices.enumerateDevices() 49 | .then(devices => { 50 | expect(devices).to.be.an('Array'); 51 | }); 52 | }); 53 | 54 | ['audioinput', 'videoinput', 'audiooutput'].forEach(kind => { 55 | it('some ' + kind + ' devices', () => { 56 | return navigator.mediaDevices.enumerateDevices() 57 | .then(devices => { 58 | expect(devices.find(d => d.kind === kind)) 59 | .not.to.equal(undefined); 60 | }); 61 | }); 62 | }); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/e2e/mediastream.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('MediaStream', () => { 12 | it('window.MediaStream exists', () => { 13 | expect(window).to.have.property('MediaStream'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/e2e/msid.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('MSID', () => { 12 | let pc1; 13 | let pc2; 14 | let localStream; 15 | 16 | function negotiate(pc, otherPc) { 17 | return pc.createOffer() 18 | .then(function(offer) { 19 | return pc.setLocalDescription(offer); 20 | }).then(function() { 21 | return otherPc.setRemoteDescription(pc.localDescription); 22 | }).then(function() { 23 | return otherPc.createAnswer(); 24 | }).then(function(answer) { 25 | return otherPc.setLocalDescription(answer); 26 | }).then(function() { 27 | return pc.setRemoteDescription(otherPc.localDescription); 28 | }); 29 | } 30 | 31 | beforeEach(() => { 32 | pc1 = new RTCPeerConnection(null); 33 | pc2 = new RTCPeerConnection(null); 34 | 35 | pc1.onicecandidate = event => pc2.addIceCandidate(event.candidate); 36 | pc2.onicecandidate = event => pc1.addIceCandidate(event.candidate); 37 | }); 38 | afterEach(() => { 39 | pc1.close(); 40 | pc2.close(); 41 | }); 42 | 43 | it('signals stream ids', (done) => { 44 | pc2.ontrack = (e) => { 45 | expect(e.streams[0].id).to.equal(localStream.id); 46 | done(); 47 | }; 48 | navigator.mediaDevices.getUserMedia({video: true}) 49 | .then((stream) => { 50 | localStream = stream; 51 | pc1.addTrack(stream.getTracks()[0], stream); 52 | return negotiate(pc1, pc2); 53 | }); 54 | }); 55 | 56 | it('puts the stream msid attribute into the localDescription', () => { 57 | return navigator.mediaDevices.getUserMedia({video: true}) 58 | .then((stream) => { 59 | localStream = stream; 60 | pc1.addTrack(stream.getTracks()[0], stream); 61 | return negotiate(pc1, pc2); 62 | }) 63 | .then(() => { 64 | expect(pc1.localDescription.sdp) 65 | .to.contain('msid:' + localStream.id + ' '); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/e2e/negotiationneeded.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('negotiationneeded event', () => { 12 | let pc; 13 | beforeEach(() => { 14 | pc = new RTCPeerConnection(); 15 | }); 16 | afterEach(() => { 17 | pc.close(); 18 | }); 19 | 20 | it('does not fire when adding a track after ' + 21 | 'setRemoteDescription', (done) => { 22 | const sdp = 'v=0\r\n' + 23 | 'o=- 166855176514521964 2 IN IP4 127.0.0.1\r\n' + 24 | 's=-\r\n' + 25 | 't=0 0\r\n' + 26 | 'a=msid-semantic:WMS *\r\n' + 27 | 'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' + 28 | 'c=IN IP4 0.0.0.0\r\n' + 29 | 'a=rtcp:9 IN IP4 0.0.0.0\r\n' + 30 | 'a=ice-ufrag:someufrag\r\n' + 31 | 'a=ice-pwd:somelongpwdwithenoughrandomness\r\n' + 32 | 'a=fingerprint:sha-256 8C:71:B3:8D:A5:38:FD:8F:A4:2E:A2:65:6C:86:52' + 33 | ':BC:E0:6E:94:F2:9F:7C:4D:B5:DF:AF:AA:6F:44:90:8D:F4\r\n' + 34 | 'a=setup:actpass\r\n' + 35 | 'a=rtcp-mux\r\n' + 36 | 'a=mid:mid1\r\n' + 37 | 'a=sendonly\r\n' + 38 | 'a=rtpmap:111 opus/48000/2\r\n' + 39 | 'a=msid:stream1 track1\r\n' + 40 | 'a=ssrc:1001 cname:some\r\n'; 41 | var onnfired = false; 42 | pc.onnegotiationneeded = () => { 43 | onnfired = true; 44 | }; 45 | pc.setRemoteDescription({type: 'offer', sdp}) 46 | .then(() => navigator.mediaDevices.getUserMedia({audio: true})) 47 | .then((stream) => pc.addTrack(stream.getTracks()[0], stream)) 48 | .then(() => { 49 | setTimeout(() => { 50 | expect(onnfired).to.equal(false); 51 | done(); 52 | }, 0); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/e2e/ontrack.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('track event', () => { 12 | let pc; 13 | beforeEach(() => { 14 | pc = new RTCPeerConnection(); 15 | }); 16 | afterEach(() => { 17 | pc.close(); 18 | }); 19 | 20 | const sdp = 'v=0\r\n' + 21 | 'o=- 166855176514521964 2 IN IP4 127.0.0.1\r\n' + 22 | 's=-\r\n' + 23 | 't=0 0\r\n' + 24 | 'a=msid-semantic:WMS *\r\n' + 25 | 'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' + 26 | 'c=IN IP4 0.0.0.0\r\n' + 27 | 'a=rtcp:9 IN IP4 0.0.0.0\r\n' + 28 | 'a=ice-ufrag:someufrag\r\n' + 29 | 'a=ice-pwd:somelongpwdwithenoughrandomness\r\n' + 30 | 'a=fingerprint:sha-256 8C:71:B3:8D:A5:38:FD:8F:A4:2E:A2:65:6C:86:52' + 31 | ':BC:E0:6E:94:F2:9F:7C:4D:B5:DF:AF:AA:6F:44:90:8D:F4\r\n' + 32 | 'a=setup:actpass\r\n' + 33 | 'a=rtcp-mux\r\n' + 34 | 'a=mid:mid1\r\n' + 35 | 'a=sendonly\r\n' + 36 | 'a=rtpmap:111 opus/48000/2\r\n' + 37 | 'a=msid:stream1 track1\r\n' + 38 | 'a=ssrc:1001 cname:some\r\n'; 39 | 40 | it('RTCPeerConnection.prototype.ontrack exists', () => { 41 | expect('ontrack' in RTCPeerConnection.prototype).to.equal(true); 42 | }); 43 | 44 | describe('is called by setRemoteDescription', () => { 45 | it('track event', (done) => { 46 | pc.addEventListener('track', () => { 47 | done(); 48 | }); 49 | pc.setRemoteDescription({type: 'offer', sdp}); 50 | }); 51 | 52 | it('ontrack', (done) => { 53 | pc.ontrack = () => { 54 | done(); 55 | }; 56 | pc.setRemoteDescription({type: 'offer', sdp}); 57 | }); 58 | }); 59 | 60 | describe('the event has', () => { 61 | it('a track', (done) => { 62 | pc.ontrack = (e) => { 63 | expect(e).to.have.property('track'); 64 | done(); 65 | }; 66 | pc.setRemoteDescription({type: 'offer', sdp}); 67 | }); 68 | 69 | it('a set of streams', (done) => { 70 | pc.ontrack = (e) => { 71 | expect(e).to.have.property('streams'); 72 | expect(e.streams).to.be.an('array'); 73 | done(); 74 | }; 75 | pc.setRemoteDescription({type: 'offer', sdp}); 76 | }); 77 | 78 | it('a receiver that is contained in the set of receivers', (done) => { 79 | pc.ontrack = (e) => { 80 | expect(e).to.have.property('receiver'); 81 | expect(e.receiver.track).to.equal(e.track); 82 | expect(pc.getReceivers()).to.contain(e.receiver); 83 | done(); 84 | }; 85 | pc.setRemoteDescription({type: 'offer', sdp}); 86 | }); 87 | it('a transceiver that has a receiver', (done) => { 88 | pc.ontrack = (e) => { 89 | expect(e).to.have.property('transceiver'); 90 | expect(e.transceiver).to.have.property('receiver'); 91 | expect(e.transceiver.receiver).to.equal(e.receiver); 92 | done(); 93 | }; 94 | pc.setRemoteDescription({type: 'offer', sdp}); 95 | }); 96 | }); 97 | 98 | it('is called when setRemoteDescription adds a new track to ' + 99 | 'an existing stream', (done) => { 100 | const videoPart = 'm=video 9 UDP/TLS/RTP/SAVPF 100\r\n' + 101 | 'c=IN IP4 0.0.0.0\r\n' + 102 | 'a=rtcp:9 IN IP4 0.0.0.0\r\n' + 103 | 'a=ice-ufrag:someufrag\r\n' + 104 | 'a=ice-pwd:somelongpwdwithenoughrandomness\r\n' + 105 | 'a=fingerprint:sha-256 8C:71:B3:8D:A5:38:FD:8F:A4:2E:A2:65:6C:86:52' + 106 | ':BC:E0:6E:94:F2:9F:7C:4D:B5:DF:AF:AA:6F:44:90:8D:F4\r\n' + 107 | 'a=setup:actpass\r\n' + 108 | 'a=rtcp-mux\r\n' + 109 | 'a=mid:mid2\r\n' + 110 | 'a=sendonly\r\n' + 111 | 'a=rtpmap:100 vp8/90000\r\n' + 112 | 'a=msid:stream1 track2\r\n' + 113 | 'a=ssrc:1002 cname:some\r\n'; 114 | let ontrackCount = 0; 115 | pc.ontrack = (e) => { 116 | ontrackCount++; 117 | if (ontrackCount === 2) { 118 | done(); 119 | } 120 | }; 121 | pc.setRemoteDescription({type: 'offer', sdp}) 122 | .then(() => pc.createAnswer()) 123 | .then((answer) => pc.setLocalDescription(answer)) 124 | .then(() => { 125 | return pc.setRemoteDescription({type: 'offer', sdp: sdp + videoPart}); 126 | }) 127 | .catch(e => console.error(e.toString())); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/e2e/removeTrack.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('removeTrack', () => { 12 | let pc; 13 | beforeEach(() => { 14 | pc = new RTCPeerConnection(); 15 | }); 16 | afterEach(() => { 17 | if (pc.signalingState !== 'closed') { 18 | pc.close(); 19 | } 20 | }); 21 | 22 | describe('throws an exception', () => { 23 | it('if the argument is a track, not a sender', () => { 24 | return navigator.mediaDevices.getUserMedia({audio: true}) 25 | .then(stream => { 26 | pc.addTrack(stream.getTracks()[0], stream); 27 | const withTrack = () => { 28 | pc.removeTrack(stream.getTracks()[0]); 29 | }; 30 | expect(withTrack).to.throw() 31 | .that.has.property('name').that.equals('TypeError'); 32 | }); 33 | }); 34 | 35 | it('if the sender does not belong to the peerconnection', () => { 36 | return navigator.mediaDevices.getUserMedia({audio: true}) 37 | .then(stream => { 38 | const pc2 = new RTCPeerConnection(); 39 | const sender = pc2.addTrack(stream.getTracks()[0], stream); 40 | const invalidSender = () => { 41 | pc.removeTrack(sender); 42 | }; 43 | expect(invalidSender).to.throw() 44 | .that.has.property('name').that.equals('InvalidAccessError'); 45 | pc2.close(); 46 | }); 47 | }); 48 | 49 | it('if the peerconnection has been closed already', () => { 50 | return navigator.mediaDevices.getUserMedia({audio: true}) 51 | .then(stream => { 52 | const sender = pc.addTrack(stream.getTracks()[0], stream); 53 | pc.close(); 54 | const afterClose = () => { 55 | pc.removeTrack(sender); 56 | }; 57 | expect(afterClose).to.throw() 58 | .that.has.property('name').that.equals('InvalidStateError'); 59 | }); 60 | }); 61 | }); 62 | 63 | it('allows removeTrack twice', () => { 64 | return navigator.mediaDevices.getUserMedia({audio: true}) 65 | .then(stream => { 66 | const sender = pc.addTrack(stream.getTracks()[0], stream); 67 | pc.removeTrack(sender); 68 | const again = () => { 69 | pc.removeTrack(sender); 70 | }; 71 | expect(again).not.to.throw(); 72 | }); 73 | }); 74 | 75 | ['addStream', 'addTrack'].forEach(variant => { 76 | describe('after ' + variant + ' for an audio/video track', () => { 77 | beforeEach(() => { 78 | return navigator.mediaDevices.getUserMedia({audio: true, video: true}) 79 | .then(stream => { 80 | if (variant === 'addStream') { 81 | pc.addStream(stream); 82 | } else { 83 | stream.getTracks().forEach(track => { 84 | pc.addTrack(track, stream); 85 | }); 86 | } 87 | }); 88 | }); 89 | 90 | describe('after removing a single track', () => { 91 | it('only a single sender with a track remains', () => { 92 | const senders = pc.getSenders(); 93 | expect(pc.getSenders()).to.have.length(2); 94 | 95 | pc.removeTrack(senders[0]); 96 | const sendersWithTrack = pc.getSenders().filter(s => s.track); 97 | expect(sendersWithTrack).to.have.length(1); 98 | }); 99 | 100 | it('the local stream remains untouched', () => { 101 | const senders = pc.getSenders(); 102 | 103 | pc.removeTrack(senders[0]); 104 | expect(pc.getLocalStreams()).to.have.length(1); 105 | expect(pc.getLocalStreams()[0].getTracks()).to.have.length(2); 106 | }); 107 | }); 108 | 109 | describe('after removing all tracks', () => { 110 | it('no senders with tracks remain', () => { 111 | const senders = pc.getSenders(); 112 | senders.forEach(sender => pc.removeTrack(sender)); 113 | const sendersWithTrack = pc.getSenders().filter(s => s.track); 114 | expect(sendersWithTrack).to.have.length(0); 115 | }); 116 | 117 | it('no local streams remain', () => { 118 | const senders = pc.getSenders(); 119 | senders.forEach(sender => pc.removeTrack(sender)); 120 | expect(pc.getLocalStreams()).to.have.length(0); 121 | }); 122 | }); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/e2e/rtcicecandidate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('RTCIceCandidate', () => { 12 | it('window.RTCIceCandidate exists', () => { 13 | expect(window).to.have.property('RTCIceCandidate'); 14 | }); 15 | 16 | describe('is augmented in', () => { 17 | it('the onicecandidate callback', (done) => { 18 | let hasAddress = false; 19 | const pc = new window.RTCPeerConnection(); 20 | pc.onicecandidate = (e) => { 21 | if (!e.candidate) { 22 | expect(hasAddress).to.equal(1); 23 | done(); 24 | } else { 25 | hasAddress |= !!e.candidate.address; 26 | } 27 | }; 28 | pc.createOffer({offerToReceiveAudio: true}) 29 | .then(offer => pc.setLocalDescription(offer)); 30 | }); 31 | 32 | it('the icecandidate event', (done) => { 33 | let hasAddress = false; 34 | const pc = new window.RTCPeerConnection(); 35 | pc.addEventListener('icecandidate', (e) => { 36 | if (!e.candidate) { 37 | expect(hasAddress).to.equal(1); 38 | done(); 39 | } else { 40 | hasAddress |= !!e.candidate.address; 41 | } 42 | }); 43 | pc.createOffer({offerToReceiveAudio: true}) 44 | .then(offer => pc.setLocalDescription(offer)); 45 | }); 46 | }); 47 | 48 | describe('with empty candidate.candidate', () => { 49 | it('does not throw', () => { 50 | const constructor = () => { 51 | return new RTCIceCandidate({sdpMid: 'foo', candidate: ''}); 52 | }; 53 | expect(constructor).not.to.throw(); 54 | }); 55 | }); 56 | 57 | describe('icecandidate eventlistener', () => { 58 | it('can be removed', () => { 59 | let wrongCalled = false; 60 | let rightCalled = false; 61 | const wrongCb = () => wrongCalled = true; 62 | const rightCb = () => rightCalled = true; 63 | const pc = new window.RTCPeerConnection(); 64 | pc.addEventListener('icecandidate', wrongCb); 65 | pc.removeEventListener('icecandidate', wrongCb); 66 | pc.addEventListener('icecandidate', rightCb); 67 | pc.addEventListener('icegatheringstatechange', () => { 68 | if (pc.iceGatheringState !== 'complete') { 69 | return; 70 | } 71 | expect(wrongCalled).to.equal(false); 72 | expect(rightCalled).to.equal(true); 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/e2e/rtcpeerconnection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('RTCPeerConnection', () => { 12 | it('window.RTCPeerConnection exists', () => { 13 | expect(window).to.have.property('RTCPeerConnection'); 14 | }); 15 | 16 | it('constructor works', () => { 17 | const constructor = () => { 18 | return new RTCPeerConnection(); 19 | }; 20 | expect(constructor).not.to.throw(); 21 | }); 22 | 23 | describe('getSenders', () => { 24 | it('exists', () => { 25 | expect(RTCPeerConnection.prototype).to.have.property('getSenders'); 26 | }); 27 | }); 28 | 29 | describe('generateCertificate', () => { 30 | it('is a static method', () => { 31 | expect(window.RTCPeerConnection).to.have.property('generateCertificate'); 32 | }); 33 | }); 34 | 35 | describe('icegatheringstatechange', () => { 36 | let pc; 37 | beforeEach(() => { 38 | pc = new RTCPeerConnection(); 39 | }); 40 | afterEach(() => { 41 | pc.close(); 42 | }); 43 | 44 | it('fires the event', (done) => { 45 | pc.addEventListener('icegatheringstatechange', () => { 46 | if (pc.iceGatheringState === 'complete') { 47 | done(); 48 | } 49 | }); 50 | pc.createOffer({offerToReceiveAudio: true}) 51 | .then(offer => pc.setLocalDescription(offer)); 52 | }); 53 | 54 | it('calls the event handler', (done) => { 55 | pc.onicegatheringstatechange = () => { 56 | if (pc.iceGatheringState === 'complete') { 57 | done(); 58 | } 59 | }; 60 | pc.createOffer({offerToReceiveAudio: true}) 61 | .then(offer => pc.setLocalDescription(offer)); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/e2e/rtcsessiondescription.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('RTCSessionDescription', () => { 12 | it('window.RTCSessionDescription exists', () => { 13 | expect(window).to.have.property('RTCSessionDescription'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/e2e/simulcast.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('simulcast', () => { 12 | let pc1; 13 | 14 | beforeEach(() => { 15 | pc1 = new RTCPeerConnection(null); 16 | }); 17 | afterEach(() => { 18 | pc1.close(); 19 | }); 20 | 21 | it('using transceivers APIs', function() { 22 | if (window.adapter.browserDetails.browser === 'safari') { 23 | this.skip(); 24 | } 25 | const constraints = {video: true}; 26 | return navigator.mediaDevices.getUserMedia(constraints) 27 | .then((stream) => { 28 | const initOpts = { 29 | sendEncodings: [ 30 | {rid: 'high'}, 31 | {rid: 'medium', scaleResolutionDownBy: 2}, 32 | {rid: 'low', scaleResolutionDownBy: 4} 33 | ] 34 | }; 35 | pc1.addTransceiver(stream.getVideoTracks()[0], initOpts); 36 | 37 | return pc1.createOffer().then((offer) => { 38 | const simulcastRegex = 39 | /a=simulcast:[\s]?send (?:rid=)?high;medium;low/g; 40 | return expect(simulcastRegex.test(offer.sdp)).to.equal(true); 41 | }); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/e2e/srcobject.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | describe('srcObject', () => { 12 | ['audio', 'video'].forEach((mediaType) => { 13 | describe('setter', () => { 14 | it('triggers loadedmetadata (' + mediaType + ')', (done) => { 15 | let constraints = {}; 16 | constraints[mediaType] = true; 17 | navigator.mediaDevices.getUserMedia(constraints) 18 | .then((stream) => { 19 | const mediaElement = document.createElement(mediaType); 20 | mediaElement.setAttribute('autoplay', 'true'); 21 | // If the srcObject shim works, we should get media 22 | // at some point. This will trigger loadedmetadata. 23 | mediaElement.addEventListener('loadedmetadata', function() { 24 | done(); 25 | }); 26 | mediaElement.srcObject = stream; 27 | }); 28 | }); 29 | }); 30 | 31 | describe('getter', () => { 32 | it('returns the stream (' + mediaType + ')', () => { 33 | let constraints = {}; 34 | constraints[mediaType] = true; 35 | return navigator.mediaDevices.getUserMedia(constraints) 36 | .then((stream) => { 37 | const mediaElement = document.createElement(mediaType); 38 | mediaElement.setAttribute('autoplay', 'true'); 39 | mediaElement.setAttribute('id', mediaType); 40 | mediaElement.srcObject = stream; 41 | expect(mediaElement.srcObject).to.have.property('id'); 42 | expect(mediaElement.srcObject.id).to.equal(stream.id); 43 | }); 44 | }); 45 | }); 46 | }); 47 | 48 | it('setting from another object works', () => { 49 | return navigator.mediaDevices.getUserMedia({video: true}) 50 | .then(stream => { 51 | const video = document.createElement('video'); 52 | video.autoplay = true; 53 | video.srcObject = stream; 54 | 55 | const video2 = document.createElement('video2'); 56 | video2.autoplay = true; 57 | video2.srcObject = video.srcObject; 58 | 59 | expect(video2.srcObject.id).to.equal(video.srcObject.id); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/getusermedia-mocha.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | /* global beforeEach, afterEach */ 10 | 11 | /* wrap navigator.getUserMedia and navigator.mediaDevices.getUserMedia 12 | * so that any streams acquired are released after each test. 13 | */ 14 | beforeEach(() => { 15 | const streams = []; 16 | const release = () => { 17 | streams.forEach((stream) => { 18 | stream.getTracks().forEach((track) => track.stop()); 19 | }); 20 | streams.length = 0; 21 | }; 22 | 23 | if (navigator.getUserMedia) { 24 | const origGetUserMedia = navigator.getUserMedia.bind(navigator); 25 | navigator.getUserMedia = (constraints, cb, eb) => { 26 | origGetUserMedia(constraints, (stream) => { 27 | streams.push(stream); 28 | if (cb) { 29 | cb.apply(null, [stream]); 30 | } 31 | }, eb); 32 | }; 33 | navigator.getUserMedia.restore = () => { 34 | navigator.getUserMedia = origGetUserMedia; 35 | release(); 36 | }; 37 | } 38 | 39 | const origMediaDevicesGetUserMedia = 40 | navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); 41 | navigator.mediaDevices.getUserMedia = (constraints) => { 42 | return origMediaDevicesGetUserMedia(constraints) 43 | .then((stream) => { 44 | streams.push(stream); 45 | return stream; 46 | }); 47 | }; 48 | navigator.mediaDevices.getUserMedia.restore = () => { 49 | navigator.mediaDevices.getUserMedia = origMediaDevicesGetUserMedia; 50 | release(); 51 | }; 52 | }); 53 | 54 | afterEach(() => { 55 | if (navigator.getUserMedia) { 56 | navigator.getUserMedia.restore(); 57 | } 58 | navigator.mediaDevices.getUserMedia.restore(); 59 | }); 60 | 61 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | 'use strict'; 10 | 11 | const os = require('os'); 12 | const path = require('path'); 13 | const puppeteerBrowsers = require('@puppeteer/browsers'); 14 | 15 | async function download(browser, version, cacheDir, platform) { 16 | const buildId = await puppeteerBrowsers 17 | .resolveBuildId(browser, platform, version); 18 | await puppeteerBrowsers.install({ 19 | browser, 20 | buildId, 21 | cacheDir, 22 | platform 23 | }); 24 | return buildId; 25 | } 26 | 27 | module.exports = async(config) => { 28 | const cacheDir = path.join(process.cwd(), 'browsers'); 29 | const platform = puppeteerBrowsers.detectBrowserPlatform(); 30 | 31 | let browsers; 32 | if (process.env.BROWSER) { 33 | if (process.env.BROWSER === 'safari') { 34 | browsers = ['Safari']; 35 | } else if (process.env.BROWSER === 'Electron') { 36 | browsers = ['electron']; 37 | } else { 38 | browsers = [process.env.BROWSER]; 39 | } 40 | } else if (os.platform() === 'darwin') { 41 | browsers = ['chrome', 'firefox', 'Safari']; 42 | } else if (os.platform() === 'win32') { 43 | browsers = ['chrome', 'firefox']; 44 | } else { 45 | browsers = ['chrome', 'firefox']; 46 | } 47 | 48 | let reporters = ['mocha']; 49 | if (process.env.CI) { 50 | // stability must be the last reporter as it munges the 51 | // exit code and always returns 0. 52 | reporters.push('stability'); 53 | } 54 | 55 | // uses Safari Technology Preview. 56 | if (browsers.includes('Safari') && os.platform() === 'darwin' && 57 | process.env.BVER === 'unstable' && !process.env.SAFARI_BIN) { 58 | process.env.SAFARI_BIN = '/Applications/Safari Technology Preview.app' + 59 | '/Contents/MacOS/Safari Technology Preview'; 60 | } 61 | 62 | if (browsers.includes('firefox')) { 63 | const buildId = await download('firefox', process.env.BVER || 'stable', 64 | cacheDir, platform); 65 | process.env.FIREFOX_BIN = puppeteerBrowsers 66 | .computeExecutablePath({browser: 'firefox', buildId, cacheDir, platform}); 67 | } 68 | if (browsers.includes('chrome')) { 69 | const buildId = await download('chrome', process.env.BVER || 'stable', 70 | cacheDir, platform); 71 | process.env.CHROME_BIN = puppeteerBrowsers 72 | .computeExecutablePath({browser: 'chrome', buildId, cacheDir, platform}); 73 | } 74 | 75 | let chromeFlags = [ 76 | '--use-fake-device-for-media-stream', 77 | '--use-fake-ui-for-media-stream', 78 | '--no-sandbox', 79 | '--headless', '--disable-gpu', '--remote-debugging-port=9222' 80 | ]; 81 | if (process.env.CHROMEEXPERIMENT !== 'false') { 82 | chromeFlags.push('--enable-experimental-web-platform-features'); 83 | } 84 | 85 | config.set({ 86 | basePath: '..', 87 | frameworks: ['browserify', 'mocha', 'chai'], 88 | files: [ 89 | 'dist/adapter_core5.js', 90 | 'test/getusermedia-mocha.js', 91 | 'test/e2e/*.js', 92 | ], 93 | exclude: [], 94 | preprocessors: { 95 | 'dist/adapter_core5.js': ['browserify'] 96 | }, 97 | reporters, 98 | port: 9876, 99 | colors: true, 100 | logLevel: config.LOG_INFO, 101 | autoWatch: false, 102 | customLaunchers: { 103 | chrome: { 104 | base: 'Chrome', 105 | flags: chromeFlags 106 | }, 107 | electron: { 108 | base: 'Electron', 109 | flags: ['--use-fake-device-for-media-stream'] 110 | }, 111 | firefox: { 112 | base: 'Firefox', 113 | prefs: { 114 | 'media.navigator.streams.fake': true, 115 | 'media.navigator.permission.disabled': true 116 | }, 117 | flags: ['-headless'] 118 | } 119 | }, 120 | singleRun: true, 121 | concurrency: Infinity, 122 | browsers, 123 | browserify: { 124 | debug: true, 125 | transform: ['brfs'], 126 | standalone: 'adapter', 127 | }, 128 | stabilityReporter: { 129 | path: 'test/e2e/expectations/' + 130 | process.env.BROWSER + 131 | (process.env.BVER ? '-' + process.env.BVER : '') + 132 | (process.env.CHROMEEXPERIMENT === 'false' ? '-no-experimental' : ''), 133 | update: process.env.UPDATE_STABILITYREPORTER || false, 134 | } 135 | }); 136 | }; 137 | -------------------------------------------------------------------------------- /test/testpage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test page for adapter.js 5 | 6 | 7 |

Test page for adapter.js

8 | 9 | The browser is: 10 |
11 | The browser version is: 12 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true, 4 | "browser": true 5 | }, 6 | "plugins": ["jest"] 7 | } 8 | -------------------------------------------------------------------------------- /test/unit/adapterfactory.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | describe('adapter factory', () => { 9 | const {adapterFactory} = require('../../dist/adapter_factory.js'); 10 | const utils = require('../../dist/utils.js'); 11 | 12 | let window; 13 | beforeEach(() => { 14 | window = { 15 | RTCPeerConnection: jest.fn(), 16 | }; 17 | }); 18 | 19 | describe('does not shim', () => { 20 | afterEach(() => { 21 | utils.detectBrowser.mockRestore(); 22 | }); 23 | ['Chrome', 'Firefox', 'Safari'].forEach(browser => { 24 | it(browser + ' when disabled', () => { 25 | jest.spyOn(utils, 'detectBrowser').mockReturnValue({ 26 | browser: browser.toLowerCase() 27 | }); 28 | let options = {}; 29 | options['shim' + browser] = false; 30 | const adapter = adapterFactory(window, options); 31 | expect(adapter).not.toHaveProperty('browserShim'); 32 | }); 33 | }); 34 | }); 35 | 36 | it('does not throw in Firefox with peerconnection disabled', () => { 37 | window = {navigator: { 38 | mozGetUserMedia: () => {}, 39 | mediaDevices: {getUserMedia: () => {}}, 40 | userAgent: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:44.0) ' + 41 | 'Gecko/20100101 Firefox/44.0' 42 | }}; 43 | const constructor = () => adapterFactory({window}); 44 | expect(constructor).not.toThrow(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/unit/addicecandidate.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The adapter.js project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | describe('addIceCandidate with null or empty candidate', () => { 10 | const shim = require('../../dist/common_shim'); 11 | let window; 12 | let origAddIceCandidate; 13 | beforeEach(() => { 14 | window = { 15 | RTCPeerConnection: jest.fn(), 16 | }; 17 | origAddIceCandidate = jest.fn(); 18 | window.RTCPeerConnection.prototype.addIceCandidate = origAddIceCandidate; 19 | }); 20 | 21 | describe('does nothing if', () => { 22 | it('RTCPeerConnection is not defined', () => { 23 | expect(() => shim.shimAddIceCandidateNullOrEmpty({}, {})).not.toThrow(); 24 | }); 25 | it('RTCPeerConnection.prototype.addIceCandidate is undefined', () => { 26 | window.RTCPeerConnection.prototype.addIceCandidate = null; 27 | expect(() => shim.shimAddIceCandidateNullOrEmpty({}, {})).not.toThrow(); 28 | }); 29 | it('the candidate argument is optional', () => { 30 | expect(window.RTCPeerConnection.prototype.addIceCandidate.length) 31 | .toBe(0); 32 | shim.shimAddIceCandidateNullOrEmpty({}, {}); 33 | expect(window.RTCPeerConnection.prototype.addIceCandidate) 34 | .toBe(origAddIceCandidate); 35 | }); 36 | }); 37 | 38 | it('changes the number of arguments', () => { 39 | window.RTCPeerConnection.prototype.addIceCandidate = 40 | (candidate) => origAddIceCandidate(candidate); 41 | shim.shimAddIceCandidateNullOrEmpty(window, {}); 42 | expect(window.RTCPeerConnection.prototype.addIceCandidate.length) 43 | .toBe(0); 44 | }); 45 | 46 | it('ignores addIceCandidate(null)', () => { 47 | window.RTCPeerConnection.prototype.addIceCandidate = 48 | (candidate) => origAddIceCandidate(candidate); 49 | shim.shimAddIceCandidateNullOrEmpty(window, {}); 50 | const pc = new window.RTCPeerConnection(); 51 | pc.addIceCandidate({candidate: '', sdpMLineIndex: 0}); 52 | expect(origAddIceCandidate.mock.calls.length).toBe(1); 53 | }); 54 | 55 | describe('Chrome behaviour', () => { 56 | let browserDetails; 57 | // Override addIceCandidate to simulate legacy behaviour. 58 | beforeEach(() => { 59 | window.RTCPeerConnection.prototype.addIceCandidate = 60 | (candidate) => origAddIceCandidate(candidate); 61 | browserDetails = {browser: 'chrome', version: '88'}; 62 | }); 63 | 64 | it('ignores {candidate: ""} before Chrome 78', () => { 65 | browserDetails.version = 77; 66 | shim.shimAddIceCandidateNullOrEmpty(window, browserDetails); 67 | 68 | const pc = new window.RTCPeerConnection(); 69 | pc.addIceCandidate({candidate: '', sdpMLineIndex: 0}); 70 | expect(origAddIceCandidate.mock.calls.length).toBe(0); 71 | }); 72 | 73 | it('passes {candidate: ""} after Chrome 78', () => { 74 | browserDetails.version = 78; 75 | shim.shimAddIceCandidateNullOrEmpty(window, browserDetails); 76 | 77 | const pc = new window.RTCPeerConnection(); 78 | pc.addIceCandidate({candidate: '', sdpMLineIndex: 0}); 79 | expect(origAddIceCandidate.mock.calls.length).toBe(1); 80 | }); 81 | }); 82 | 83 | describe('Firefox behaviour', () => { 84 | let browserDetails; 85 | // Override addIceCandidate to simulate legacy behaviour. 86 | beforeEach(() => { 87 | window.RTCPeerConnection.prototype.addIceCandidate = 88 | (candidate) => origAddIceCandidate(candidate); 89 | browserDetails = {browser: 'firefox', version: '69'}; 90 | }); 91 | 92 | it('ignores {candidate: ""} before Firefox 68', () => { 93 | browserDetails.version = 67; 94 | shim.shimAddIceCandidateNullOrEmpty(window, browserDetails); 95 | 96 | const pc = new window.RTCPeerConnection(); 97 | pc.addIceCandidate({candidate: '', sdpMLineIndex: 0}); 98 | expect(origAddIceCandidate.mock.calls.length).toBe(0); 99 | }); 100 | 101 | it('passes {candidate: ""} after Firefox 68', () => { 102 | browserDetails.version = 68; 103 | shim.shimAddIceCandidateNullOrEmpty(window, browserDetails); 104 | 105 | const pc = new window.RTCPeerConnection(); 106 | pc.addIceCandidate({candidate: '', sdpMLineIndex: 0}); 107 | expect(origAddIceCandidate.mock.calls.length).toBe(1); 108 | }); 109 | }); 110 | 111 | describe('Safari behaviour', () => { 112 | let browserDetails; 113 | // Override addIceCandidate to simulate legacy behaviour. 114 | beforeEach(() => { 115 | window.RTCPeerConnection.prototype.addIceCandidate = 116 | (candidate) => origAddIceCandidate(candidate); 117 | browserDetails = {browser: 'safari', version: 'some'}; 118 | }); 119 | 120 | it('ignores {candidate: ""}', () => { 121 | shim.shimAddIceCandidateNullOrEmpty(window, browserDetails); 122 | 123 | const pc = new window.RTCPeerConnection(); 124 | pc.addIceCandidate({candidate: '', sdpMLineIndex: 0}); 125 | expect(origAddIceCandidate.mock.calls.length).toBe(0); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /test/unit/chrome.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* a mock of the Chrome RTCLegacyStatReport */ 10 | function RTCLegacyStatsReport() { 11 | this.id = 'someid'; 12 | this.type = 'set-me'; 13 | this.timestamp = new Date(); 14 | this._data = {}; 15 | } 16 | RTCLegacyStatsReport.prototype.names = function() { 17 | return Object.keys(this._data); 18 | }; 19 | RTCLegacyStatsReport.prototype.stat = function(name) { 20 | return this._data[name]; 21 | }; 22 | 23 | describe('Chrome shim', () => { 24 | const shim = require('../../dist/chrome/chrome_shim'); 25 | let window; 26 | 27 | beforeEach(() => { 28 | window = { 29 | navigator: { 30 | mediaDevices: { 31 | getUserMedia: jest.fn().mockReturnValue(Promise.resolve('stream')), 32 | }, 33 | }, 34 | RTCPeerConnection: function() {} 35 | }; 36 | }); 37 | 38 | describe('PeerConnection shim', () => { 39 | it('fail silently if RTCPeerConnection is not present', () => { 40 | window = {}; 41 | 42 | shim.shimPeerConnection(window); 43 | }); 44 | }); 45 | 46 | describe('AddTrackRemoveTrack shim', () => { 47 | it('fail silently if RTCPeerConnection is not present', () => { 48 | window = {}; 49 | 50 | shim.shimAddTrackRemoveTrack(window); 51 | }); 52 | }); 53 | 54 | describe('getUserMedia shim', () => { 55 | it('fail silently if navigator.mediaDevices is not present', () => { 56 | window = { 57 | navigator: {} 58 | }; 59 | 60 | shim.shimGetUserMedia(window); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/unit/compactObject.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | describe('compactObject', () => { 10 | const compactObject = require('../../dist/utils.js').compactObject; 11 | 12 | it('returns an empty object as is', () => { 13 | expect(compactObject({})).toEqual({}); 14 | }); 15 | 16 | it('removes undefined values', () => { 17 | expect(compactObject({ 18 | nothing: undefined, 19 | value: 'hello', 20 | something: undefined, 21 | })).toEqual({ 22 | value: 'hello', 23 | }); 24 | }); 25 | 26 | it('removes nested empty objects', () => { 27 | expect(compactObject({ 28 | nothing: {}, 29 | val: 12, 30 | })).toEqual({ 31 | val: 12, 32 | }); 33 | }); 34 | 35 | it('removes nested undefined values', () => { 36 | expect(compactObject({ 37 | value: 'hello', 38 | something: { 39 | nestedValue: 12, 40 | nestedEmpty: {}, 41 | nestedNothing: undefined, 42 | }, 43 | })).toEqual({ 44 | value: 'hello', 45 | something: { 46 | nestedValue: 12, 47 | }, 48 | }); 49 | }); 50 | 51 | it('leaves arrays alone', () => { 52 | const arr = [{val: 'hello'}, undefined, 525]; 53 | expect(compactObject({ 54 | nothing: undefined, 55 | value: arr, 56 | something: undefined, 57 | })).toEqual({ 58 | value: arr, 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/unit/detectBrowser.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | describe('detectBrowser', () => { 9 | const detectBrowser = require('../../dist/utils.js').detectBrowser; 10 | let window; 11 | let navigator; 12 | 13 | beforeEach(() => { 14 | navigator = {}; 15 | window = {navigator}; 16 | }); 17 | 18 | it('detects Firefox if navigator.mozGetUserMedia exists', () => { 19 | navigator.userAgent = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; ' + 20 | 'rv:44.0) Gecko/20100101 Firefox/44.0'; 21 | navigator.mozGetUserMedia = function() {}; 22 | 23 | const browserDetails = detectBrowser(window); 24 | expect(browserDetails.browser).toEqual('firefox'); 25 | expect(browserDetails.version).toEqual(44); 26 | }); 27 | 28 | it('detects Chrome if navigator.webkitGetUserMedia exists', () => { 29 | navigator.userAgent = 'Mozilla/5.0 (X11; Linux x86_64) ' + 30 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 ' + 31 | 'Safari/537.36'; 32 | navigator.webkitGetUserMedia = function() {}; 33 | window.webkitRTCPeerConnection = function() {}; 34 | 35 | const browserDetails = detectBrowser(window); 36 | expect(browserDetails.browser).toEqual('chrome'); 37 | expect(browserDetails.version).toEqual(45); 38 | }); 39 | 40 | it('detects chrome with reduced useragent', () => { 41 | navigator.userAgent = 'Mozilla/5.0 (X11; Linux x86_64) ' + 42 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.0.0 ' + 43 | 'Safari/537.36'; 44 | navigator.webkitGetUserMedia = function() {}; 45 | window.webkitRTCPeerConnection = function() {}; 46 | 47 | const browserDetails = detectBrowser(window); 48 | expect(browserDetails.browser).toEqual('chrome'); 49 | expect(browserDetails.version).toEqual(95); 50 | }); 51 | 52 | it('detects Chrome if navigator.userAgentData exists', () => { 53 | navigator.userAgentData = {brands: [{brand: 'Chromium', version: '102'}]}; 54 | // Use the wrong UA string for Firefox. 55 | navigator.userAgent = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; ' + 56 | 'rv:44.0) Gecko/20100101 Firefox/44.0'; 57 | navigator.mozGetUserMedia = function() {}; 58 | 59 | const browserDetails = detectBrowser(window); 60 | expect(browserDetails.browser).toEqual('chrome'); 61 | expect(browserDetails.version).toEqual(102); 62 | }); 63 | 64 | it('detects Safari if window.RTCPeerConnection exists', () => { 65 | navigator.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) ' + 66 | 'AppleWebKit/604.1.6 (KHTML, like Gecko) Version/10.2 Safari/604.1.6'; 67 | window.RTCPeerConnection = function() {}; 68 | 69 | const browserDetails = detectBrowser(window); 70 | expect(browserDetails.browser).toEqual('safari'); 71 | expect(browserDetails.version).toEqual(604); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/unit/extmap-allow-mixed.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The adapter.js project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | describe('removal of extmap-allow-mixed', () => { 9 | const shim = require('../../dist/common_shim'); 10 | let window; 11 | let origSetRemoteDescription; 12 | beforeEach(() => { 13 | window = { 14 | RTCPeerConnection: jest.fn(), 15 | }; 16 | origSetRemoteDescription = jest.fn(); 17 | window.RTCPeerConnection.prototype.setRemoteDescription = 18 | origSetRemoteDescription; 19 | }); 20 | 21 | const sdp = 'a=extmap-allow-mixed\r\n'; 22 | 23 | describe('does nothing if', () => { 24 | it('RTCPeerConnection is not defined', () => { 25 | expect(() => shim.removeExtmapAllowMixed({}, {})).not.toThrow(); 26 | }); 27 | }); 28 | 29 | describe('Chrome behaviour', () => { 30 | let browserDetails; 31 | // Override addIceCandidate to simulate legacy behaviour. 32 | beforeEach(() => { 33 | window.RTCPeerConnection.prototype.setRemoteDescription = function() { 34 | return origSetRemoteDescription.apply(this, arguments); 35 | }; 36 | browserDetails = {browser: 'chrome', version: '88'}; 37 | }); 38 | 39 | it('does not remove the extmap-allow-mixed line after Chrome 71', () => { 40 | browserDetails.version = 71; 41 | shim.removeExtmapAllowMixed(window, browserDetails); 42 | 43 | const pc = new window.RTCPeerConnection(); 44 | pc.setRemoteDescription({sdp: '\n' + sdp}); 45 | expect(origSetRemoteDescription.mock.calls.length).toBe(1); 46 | expect(origSetRemoteDescription.mock.calls[0][0].sdp) 47 | .toEqual('\n' + sdp); 48 | }); 49 | 50 | it('does remove the extmap-allow-mixed line before Chrome 71', () => { 51 | browserDetails.version = 70; 52 | shim.removeExtmapAllowMixed(window, browserDetails); 53 | 54 | const pc = new window.RTCPeerConnection(); 55 | pc.setRemoteDescription({sdp: '\n' + sdp}); 56 | expect(origSetRemoteDescription.mock.calls.length).toBe(1); 57 | expect(origSetRemoteDescription.mock.calls[0][0].sdp) 58 | .toEqual('\n'); 59 | }); 60 | }); 61 | 62 | describe('Safari behaviour', () => { 63 | let browserDetails; 64 | // Override addIceCandidate to simulate legacy behaviour. 65 | beforeEach(() => { 66 | window.RTCPeerConnection.prototype.setRemoteDescription = function() { 67 | return origSetRemoteDescription.apply(this, arguments); 68 | }; 69 | browserDetails = {browser: 'safari', version: '605'}; 70 | }); 71 | 72 | it('does not remove the extmap-allow-mixed line after 605', () => { 73 | browserDetails.version = 605; 74 | shim.removeExtmapAllowMixed(window, browserDetails); 75 | 76 | const pc = new window.RTCPeerConnection(); 77 | pc.setRemoteDescription({sdp: '\n' + sdp}); 78 | expect(origSetRemoteDescription.mock.calls.length).toBe(1); 79 | expect(origSetRemoteDescription.mock.calls[0][0].sdp) 80 | .toEqual('\n' + sdp); 81 | }); 82 | 83 | it('does remove the extmap-allow-mixed line before 605', () => { 84 | browserDetails.version = 604; 85 | shim.removeExtmapAllowMixed(window, browserDetails); 86 | 87 | const pc = new window.RTCPeerConnection(); 88 | pc.setRemoteDescription({sdp: '\n' + sdp}); 89 | expect(origSetRemoteDescription.mock.calls.length).toBe(1); 90 | expect(origSetRemoteDescription.mock.calls[0][0].sdp) 91 | .toEqual('\n'); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test/unit/extractVersion.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | describe('extractVersion', () => { 9 | const extractVersion = require('../../dist/utils.js').extractVersion; 10 | 11 | let ua; 12 | describe('Chrome regular expression', () => { 13 | const expr = /Chrom(e|ium)\/(\d+)\./; 14 | 15 | it('matches Chrome', () => { 16 | ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like ' + 17 | 'Gecko) Chrome/45.0.2454.101 Safari/537.36'; 18 | expect(extractVersion(ua, expr, 2)).toBe(45); 19 | }); 20 | 21 | it('matches Chrome 100+', () => { 22 | ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like ' + 23 | 'Gecko) Chrome/100.0.2454.101 Safari/537.36'; 24 | expect(extractVersion(ua, expr, 2)).toBe(100); 25 | }); 26 | 27 | it('matches Chromium', () => { 28 | ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like ' + 29 | 'Gecko) Ubuntu Chromium/45.0.2454.85 Chrome/45.0.2454.85 ' + 30 | 'Safari/537.36'; 31 | expect(extractVersion(ua, expr, 2)).toBe(45); 32 | }); 33 | 34 | it('matches Chrome on Android', () => { 35 | ua = 'Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) ' + 36 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 ' + 37 | 'Safari/537.36'; 38 | expect(extractVersion(ua, expr, 2)).toBe(42); 39 | }); 40 | 41 | it('recognizes Opera as Chrome', () => { 42 | // Opera, should match chrome/webrtc version 45.0 not Opera 32.0. 43 | ua = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, ' + 44 | 'like Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.44'; 45 | expect(extractVersion(ua, /Chrom(e|ium)\/(\d+)\./, 2)).toBe(45); 46 | }); 47 | 48 | it('does not match Firefox', () => { 49 | ua = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:44.0) Gecko/20100101 ' + 50 | 'Firefox/44.0'; 51 | expect(extractVersion(ua, expr, 2)).toBe(null); 52 | }); 53 | 54 | it('does not match Safari', () => { 55 | ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) ' + 56 | 'AppleWebKit/604.1.6 (KHTML, like Gecko) Version/10.2 Safari/604.1.6'; 57 | expect(extractVersion(ua, expr, 2)).toBe(null); 58 | }); 59 | 60 | it('does match Edge (by design, do not use for Edge)', () => { 61 | ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + 62 | '(KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10547'; 63 | expect(extractVersion(ua, expr, 2)).toBe(46); 64 | }); 65 | 66 | it('does not match non-Chrome', () => { 67 | ua = 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) ' + 68 | 'AppleWebKit/535.19 KHTML, like Gecko) Silk/3.13 Safari/535.19 ' + 69 | 'Silk-Accelerated=true'; 70 | expect(extractVersion(ua, expr, 2)).toBe(null); 71 | }); 72 | 73 | it('does not match the iPhone simulator', () => { 74 | ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) ' + 75 | 'AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 ' + 76 | 'Mobile/12A4345d Safari/600.1.4'; 77 | expect(extractVersion(ua, expr, 1)).toBe(null); 78 | }); 79 | }); 80 | 81 | describe('Firefox regular expression', () => { 82 | const expr = /Firefox\/(\d+)\./; 83 | it('matches Firefox', () => { 84 | ua = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:44.0) Gecko/20100101 ' + 85 | 'Firefox/44.0'; 86 | expect(extractVersion(ua, expr, 1)).toBe(44); 87 | }); 88 | 89 | it('matches Firefox 100+', () => { 90 | ua = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:44.0) Gecko/20100101 ' + 91 | 'Firefox/100.0'; 92 | expect(extractVersion(ua, expr, 1)).toBe(100); 93 | }); 94 | 95 | it('does not match Chrome', () => { 96 | ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like ' + 97 | 'Gecko) Chrome/45.0.2454.101 Safari/537.36'; 98 | expect(extractVersion(ua, expr, 1)).toBe(null); 99 | }); 100 | 101 | it('does not match Safari', () => { 102 | ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) ' + 103 | 'AppleWebKit/604.1.6 (KHTML, like Gecko) Version/10.2 Safari/604.1.6'; 104 | expect(extractVersion(ua, expr, 1)).toBe(null); 105 | }); 106 | 107 | it('does not match Edge', () => { 108 | ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + 109 | '(KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10547'; 110 | expect(extractVersion(ua, expr, 1)).toBe(null); 111 | }); 112 | }); 113 | 114 | describe('Safari regular expression', () => { 115 | const expr = /AppleWebKit\/(\d+)/; 116 | it('matches the webkit version', () => { 117 | ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) ' + 118 | 'AppleWebKit/604.1.6 (KHTML, like Gecko) Version/10.2 Safari/604.1.6'; 119 | expect(extractVersion(ua, expr, 1)).toBe(604); 120 | }); 121 | 122 | it('matches the iphone simulator', () => { 123 | ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) ' + 124 | 'AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 ' + 125 | 'Mobile/12A4345d Safari/600.1.4'; 126 | expect(extractVersion(ua, expr, 1)).toBe(600); 127 | }); 128 | 129 | it('matches Chrome (by design, do not use for Chrome)', () => { 130 | ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like ' + 131 | 'Gecko) Chrome/45.0.2454.101 Safari/537.36'; 132 | expect(extractVersion(ua, expr, 1)).toBe(537); 133 | }); 134 | 135 | it('matches Edge (by design, do not use for Edge', () => { 136 | ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + 137 | '(KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10547'; 138 | expect(extractVersion(ua, expr, 1)).toBe(537); 139 | }); 140 | 141 | it('does not match Firefox', () => { 142 | ua = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:44.0) Gecko/20100101 ' + 143 | 'Firefox/44.0'; 144 | expect(extractVersion(ua, expr, 1)).toBe(null); 145 | }); 146 | }); 147 | }); 148 | 149 | -------------------------------------------------------------------------------- /test/unit/firefox.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | describe('Firefox shim', () => { 9 | const shim = require('../../dist/firefox/firefox_shim'); 10 | let window; 11 | 12 | beforeEach(() => { 13 | window = { 14 | navigator: { 15 | mediaDevices: { 16 | getUserMedia: jest.fn(), 17 | }, 18 | }, 19 | }; 20 | }); 21 | 22 | describe('getDisplayMedia shim', () => { 23 | it('does not if navigator.mediaDevices does not exist', () => { 24 | delete window.navigator.mediaDevices; 25 | shim.shimGetDisplayMedia(window); 26 | expect(window.navigator.mediaDevices).toBe(undefined); 27 | }); 28 | 29 | it('does not overwrite an existing ' + 30 | 'navigator.mediaDevices.getDisplayMedia', () => { 31 | window.navigator.mediaDevices.getDisplayMedia = 'foo'; 32 | shim.shimGetDisplayMedia(window, 'screen'); 33 | expect(window.navigator.mediaDevices.getDisplayMedia).toBe('foo'); 34 | }); 35 | 36 | it('shims navigator.mediaDevices.getDisplayMedia', () => { 37 | shim.shimGetDisplayMedia(window, 'screen'); 38 | expect(typeof window.navigator.mediaDevices.getDisplayMedia) 39 | .toBe('function'); 40 | }); 41 | 42 | ['screen', 'window'].forEach((mediaSource) => { 43 | it('calls getUserMedia with the given default mediaSource', () => { 44 | shim.shimGetDisplayMedia(window, mediaSource); 45 | window.navigator.mediaDevices.getDisplayMedia({video: true}); 46 | expect(window.navigator.mediaDevices.getUserMedia.mock.calls.length) 47 | .toBe(1); 48 | expect(window.navigator.mediaDevices.getUserMedia.mock.calls[0][0]) 49 | .toEqual({video: {mediaSource}}); 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/unit/logSuppression.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | describe('Log suppression', () => { 10 | const utils = require('../../dist/utils.js'); 11 | beforeEach(() => { 12 | jest.spyOn(console, 'log'); 13 | global.window = {}; 14 | require('../../out/adapter.js'); 15 | }); 16 | 17 | afterEach(() => { 18 | delete global.window; 19 | }); 20 | 21 | it('does not call console.log by default', () => { 22 | utils.log('test'); 23 | expect(console.log.mock.calls.length).toBe(0); 24 | }); 25 | it('does call console.log when enabled', () => { 26 | utils.disableLog(false); 27 | utils.log('test'); 28 | expect(console.log.mock.calls.length).toBe(1); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/unit/rtcicecandidate.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | /* eslint-env node */ 9 | describe('RTCIceCandidate', () => { 10 | const shim = require('../../dist/common_shim'); 11 | let RTCIceCandidate; 12 | let window; 13 | beforeEach(() => { 14 | window = {}; 15 | window.RTCIceCandidate = function(args) { 16 | return args; 17 | }; 18 | window.RTCPeerConnection = function() {}; 19 | shim.shimRTCIceCandidate(window); 20 | 21 | RTCIceCandidate = window.RTCIceCandidate; 22 | }); 23 | 24 | const candidateString = 'candidate:702786350 2 udp 41819902 8.8.8.8 60769 ' + 25 | 'typ relay raddr 8.8.8.8 rport 1234 ' + 26 | 'tcptype active ' + 27 | 'ufrag abc ' + 28 | 'generation 0'; 29 | 30 | describe('constructor', () => { 31 | it('retains the candidate', () => { 32 | const candidate = new RTCIceCandidate({ 33 | candidate: candidateString, 34 | sdpMid: 'audio', 35 | sdpMLineIndex: 0 36 | }); 37 | expect(candidate.candidate).toBe(candidateString); 38 | expect(candidate.sdpMid).toBe('audio'); 39 | expect(candidate.sdpMLineIndex).toBe(0); 40 | }); 41 | 42 | it('drops the a= part of the candidate if present', () => { 43 | const candidate = new RTCIceCandidate({ 44 | candidate: 'a=' + candidateString, 45 | sdpMid: 'audio', 46 | sdpMLineIndex: 0 47 | }); 48 | expect(candidate.candidate).toBe(candidateString); 49 | }); 50 | 51 | it('parses the candidate', () => { 52 | const candidate = new RTCIceCandidate({ 53 | candidate: candidateString, 54 | sdpMid: 'audio', 55 | sdpMLineIndex: 0 56 | }); 57 | expect(candidate.foundation).toBe('702786350'); 58 | expect(candidate.component).toBe('rtcp'); 59 | expect(candidate.priority).toBe(41819902); 60 | expect(candidate.ip).toBe('8.8.8.8'); 61 | expect(candidate.protocol).toBe('udp'); 62 | expect(candidate.port).toBe(60769); 63 | expect(candidate.type).toBe('relay'); 64 | expect(candidate.tcpType).toBe('active'); 65 | expect(candidate.relatedAddress).toBe('8.8.8.8'); 66 | expect(candidate.relatedPort).toBe(1234); 67 | expect(candidate.generation).toBe('0'); 68 | expect(candidate.usernameFragment).toBe('abc'); 69 | }); 70 | }); 71 | 72 | it('does not serialize the extra attributes', () => { 73 | const candidate = new RTCIceCandidate({ 74 | candidate: candidateString, 75 | sdpMid: 'audio', 76 | sdpMLineIndex: 0, 77 | usernameFragment: 'someufrag' 78 | }); 79 | const serialized = JSON.stringify(candidate); 80 | // there should be only 4 items in the JSON. 81 | expect(Object.keys(JSON.parse(serialized)).length).toBe(4); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/unit/safari.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | describe('Safari shim', () => { 9 | const shim = require('../../dist/safari/safari_shim'); 10 | let window; 11 | 12 | beforeEach(() => { 13 | window = { 14 | RTCPeerConnection: jest.fn() 15 | }; 16 | }); 17 | 18 | describe('shimStreamsAPI', () => { 19 | beforeEach(() => { 20 | window.RTCPeerConnection.prototype.addTrack = jest.fn(); 21 | shim.shimLocalStreamsAPI(window); 22 | shim.shimRemoteStreamsAPI(window); 23 | }); 24 | 25 | it('shimStreamsAPI existence', () => { 26 | const prototype = window.RTCPeerConnection.prototype; 27 | expect(prototype.addTrack.length).toBe(1); 28 | expect(prototype.addStream.length).toBe(1); 29 | expect(prototype.removeStream.length).toBe(1); 30 | expect(prototype.getLocalStreams.length).toBe(0); 31 | expect(prototype.getRemoteStreams.length).toBe(0); 32 | }); 33 | it('local streams API', () => { 34 | const pc = new window.RTCPeerConnection(); 35 | pc.getSenders = () => []; 36 | const stream = { 37 | id: 'id1', 38 | getTracks: () => [], 39 | getAudioTracks: () => [], 40 | getVideoTracks: () => [], 41 | }; 42 | expect(pc.getLocalStreams().length).toBe(0); 43 | expect(pc.getRemoteStreams().length).toBe(0); 44 | 45 | pc.addStream(stream); 46 | expect(pc.getLocalStreams()[0]).toBe(stream); 47 | expect(pc.getRemoteStreams().length).toBe(0); 48 | 49 | const stream2 = { 50 | id: 'id2', 51 | getTracks: () => [], 52 | getAudioTracks: () => [], 53 | getVideoTracks: () => [], 54 | }; 55 | pc.removeStream(stream2); 56 | expect(pc.getLocalStreams()[0]).toBe(stream); 57 | 58 | pc.addTrack({}, stream2); 59 | expect(pc.getLocalStreams().length).toBe(2); 60 | expect(pc.getLocalStreams()[0]).toBe(stream); 61 | expect(pc.getLocalStreams()[1]).toBe(stream2); 62 | 63 | pc.removeStream(stream2); 64 | expect(pc.getLocalStreams().length).toBe(1); 65 | expect(pc.getLocalStreams()[0]).toBe(stream); 66 | 67 | pc.removeStream(stream); 68 | expect(pc.getLocalStreams().length).toBe(0); 69 | }); 70 | }); 71 | 72 | describe('shimCallbacksAPI', () => { 73 | it('shimCallbacksAPI existence', () => { 74 | shim.shimCallbacksAPI(window); 75 | const prototype = window.RTCPeerConnection.prototype; 76 | expect(prototype.createOffer.length).toBe(2); 77 | expect(prototype.createAnswer.length).toBe(2); 78 | expect(prototype.setLocalDescription.length).toBe(3); 79 | expect(prototype.setRemoteDescription.length).toBe(3); 80 | expect(prototype.addIceCandidate.length).toBe(3); 81 | }); 82 | }); 83 | 84 | ['createOffer', 'createAnswer'].forEach((method) => { 85 | describe('legacy ' + method + ' shim', () => { 86 | describe('options passing with', () => { 87 | let stub; 88 | beforeEach(() => { 89 | stub = jest.fn(); 90 | window.RTCPeerConnection.prototype[method] = stub; 91 | shim.shimCallbacksAPI(window); 92 | }); 93 | 94 | it('no arguments', () => { 95 | const pc = new window.RTCPeerConnection(); 96 | pc[method](); 97 | expect(stub.mock.calls.length).toBe(1); 98 | expect(stub.mock.calls[0]).toEqual([undefined]); 99 | }); 100 | 101 | it('two callbacks', () => { 102 | const pc = new window.RTCPeerConnection(); 103 | pc[method](null, null); 104 | expect(stub.mock.calls.length).toBe(1); 105 | expect(stub.mock.calls[0]).toEqual([undefined]); 106 | }); 107 | 108 | it('a non-function first argument', () => { 109 | const pc = new window.RTCPeerConnection(); 110 | pc[method](1); 111 | expect(stub.mock.calls.length).toBe(1); 112 | expect(stub.mock.calls[0]).toEqual([1]); 113 | }); 114 | 115 | it('two callbacks and options', () => { 116 | const pc = new window.RTCPeerConnection(); 117 | pc[method](null, null, 1); 118 | expect(stub.mock.calls.length).toBe(1); 119 | expect(stub.mock.calls[0]).toEqual([1]); 120 | }); 121 | 122 | it('two callbacks and two additional arguments', () => { 123 | const pc = new window.RTCPeerConnection(); 124 | pc[method](null, null, 1, 2); 125 | expect(stub.mock.calls.length).toBe(1); 126 | expect(stub.mock.calls[0]).toEqual([1]); 127 | }); 128 | }); 129 | }); 130 | }); 131 | 132 | describe('legacy createOffer shim converts offer into transceivers', () => { 133 | let pc, stub, options; 134 | beforeEach(() => { 135 | stub = jest.fn(); 136 | window.RTCPeerConnection.prototype.createOffer = function() {}; 137 | shim.shimCreateOfferLegacy(window); 138 | 139 | pc = new window.RTCPeerConnection(); 140 | pc.getTransceivers = function() { 141 | return []; 142 | }; 143 | pc.addTransceiver = stub; 144 | 145 | options = { 146 | offerToReceiveAudio: false, 147 | offerToReceiveVideo: false, 148 | }; 149 | }); 150 | 151 | it('when offerToReceive Audio is true', () => { 152 | options.offerToReceiveAudio = true; 153 | pc.createOffer(options); 154 | expect(stub.mock.calls.length).toBe(1); 155 | expect(stub.mock.calls[0]).toEqual(['audio', {direction: 'recvonly'}]); 156 | }); 157 | 158 | it('when offerToReceive Video is true', () => { 159 | options.offerToReceiveVideo = true; 160 | pc.createOffer(options); 161 | expect(stub.mock.calls.length).toBe(1); 162 | expect(stub.mock.calls[0]).toEqual(['video', {direction: 'recvonly'}]); 163 | }); 164 | 165 | it('when both offers are false', () => { 166 | pc.createOffer(options); 167 | expect(stub.mock.calls.length).toBe(0); 168 | }); 169 | 170 | it('when both offers are true', () => { 171 | options.offerToReceiveAudio = true; 172 | options.offerToReceiveVideo = true; 173 | pc.createOffer(options); 174 | expect(stub.mock.calls.length).toBe(2); 175 | expect(stub.mock.calls[0]).toEqual(['audio', {direction: 'recvonly'}]); 176 | expect(stub.mock.calls[1]).toEqual(['video', {direction: 'recvonly'}]); 177 | }); 178 | 179 | it('when offerToReceive has bit values', () => { 180 | options.offerToReceiveAudio = 0; 181 | options.offerToReceiveVideo = 1; 182 | pc.createOffer(options); 183 | expect(stub.mock.calls.length).toBe(1); 184 | expect(stub.mock.calls[0]).toEqual(['video', {direction: 'recvonly'}]); 185 | }); 186 | }); 187 | 188 | describe('conversion of RTCIceServer.url', () => { 189 | let nativeStub; 190 | beforeEach(() => { 191 | nativeStub = jest.spyOn(window, 'RTCPeerConnection'); 192 | shim.shimRTCIceServerUrls(window); 193 | }); 194 | 195 | const stunURL = 'stun:stun.l.google.com:19302'; 196 | const url = {url: stunURL}; 197 | const urlArray = {url: [stunURL]}; 198 | const urls = {urls: stunURL}; 199 | const urlsArray = {urls: [stunURL]}; 200 | 201 | describe('does not modify RTCIceServer.urls', () => { 202 | it('for strings', () => { 203 | new window.RTCPeerConnection({iceServers: [urls]}); 204 | expect(nativeStub.mock.calls.length).toBe(1); 205 | expect(nativeStub.mock.calls[0][0]).toEqual({ 206 | iceServers: [urls], 207 | }); 208 | }); 209 | 210 | it('for arrays', () => { 211 | new window.RTCPeerConnection({iceServers: [urlsArray]}); 212 | expect(nativeStub.mock.calls.length).toBe(1); 213 | expect(nativeStub.mock.calls[0][0]).toEqual({ 214 | iceServers: [urlsArray], 215 | }); 216 | }); 217 | }); 218 | 219 | describe('transforms RTCIceServer.url to RTCIceServer.urls', () => { 220 | it('for strings', () => { 221 | new window.RTCPeerConnection({iceServers: [url]}); 222 | expect(nativeStub.mock.calls.length).toBe(1); 223 | expect(nativeStub.mock.calls[0][0]).toEqual({ 224 | iceServers: [urls], 225 | }); 226 | }); 227 | 228 | it('for arrays', () => { 229 | new window.RTCPeerConnection({iceServers: [urlArray]}); 230 | expect(nativeStub.mock.calls.length).toBe(1); 231 | expect(nativeStub.mock.calls[0][0]).toEqual({ 232 | iceServers: [urlsArray], 233 | }); 234 | }); 235 | }); 236 | }); 237 | }); 238 | --------------------------------------------------------------------------------