├── .npmrc
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── bug_report.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── lint-and-unit-tests.yml
│ └── e2e-tests.yaml
├── test
├── unit
│ ├── .eslintrc
│ ├── logSuppression.test.js
│ ├── compactObject.test.js
│ ├── adapterfactory.test.js
│ ├── chrome.test.js
│ ├── firefox.test.js
│ ├── rtcicecandidate.test.js
│ ├── detectBrowser.test.js
│ ├── extmap-allow-mixed.test.js
│ ├── addicecandidate.test.js
│ ├── extractVersion.test.js
│ └── safari.test.js
├── e2e
│ ├── .eslintrc
│ ├── mediastream.js
│ ├── rtcsessiondescription.js
│ ├── getusermedia.js
│ ├── browserdetails.js
│ ├── getStats.js
│ ├── simulcast.js
│ ├── negotiationneeded.js
│ ├── rtcpeerconnection.js
│ ├── addIceCandidate.js
│ ├── msid.js
│ ├── mediaDevices.js
│ ├── srcobject.js
│ ├── rtcicecandidate.js
│ ├── ontrack.js
│ ├── removeTrack.js
│ ├── addTrack.js
│ ├── dtmf.js
│ └── connection.js
├── .eslintrc
├── testpage.html
├── getusermedia-mocha.js
├── karma.conf.js
└── README.md
├── .gitignore
├── .npmignore
├── src
└── js
│ ├── adapter_core.js
│ ├── adapter_core5.js
│ ├── firefox
│ ├── getdisplaymedia.js
│ ├── getusermedia.js
│ └── firefox_shim.js
│ ├── adapter_factory.js
│ ├── chrome
│ ├── getusermedia.js
│ └── chrome_shim.js
│ ├── utils.js
│ ├── safari
│ └── safari_shim.js
│ └── common_shim.js
├── bower.json
├── CONTRIBUTING.md
├── LICENSE.md
├── .eslintrc
├── Gruntfile.js
├── index.d.ts
├── package.json
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | **Description**
2 |
3 |
4 | **Purpose**
5 |
--------------------------------------------------------------------------------
/test/unit/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true,
4 | "browser": true
5 | },
6 | "plugins": ["jest"]
7 | }
8 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/test/e2e/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true,
4 | "browser": true
5 | },
6 | "rules": {},
7 | "globals": {
8 | "expect": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es6": true
5 | },
6 | "rules": {},
7 | "parserOptions": {
8 | "ecmaVersion": 2022
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | # Please read first!
11 | Please use [discuss-webrtc](https://groups.google.com/forum/#!forum/discuss-webrtc) for general technical discussions and questions.
12 |
13 | - [ ] I have provided steps to reproduce (e.g. a link to a [jsfiddle](https://jsfiddle.net/))
14 | - [ ] I have provided browser name, version and adapter.js version
15 | - [ ] This issue only happens when adapter.js is used
16 |
17 | **Note: If the checkboxes above are not checked (which you do after the issue is posted), the issue will be closed.**
18 |
19 | ## Versions affected
20 |
21 | **Browser name including version (e.g. Chrome 64.0.3282.119)**
22 |
23 |
24 | **adapter.js (e.g. 6.1.0)**
25 |
26 |
27 | ## Description
28 |
29 |
30 | ## Steps to reproduce
31 |
32 |
33 | ## Expected results
34 |
35 |
36 | ## Actual results
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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'].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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webrtc-adapter",
3 | "version": "9.0.3",
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": "^0.0.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 | "mocha": "^10.2.0"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/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/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 | expect(browserDetails._safariVersion).toEqual(10.2);
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/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 13.1', () => {
73 | browserDetails._safariVersion = 13.1;
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 13.1', () => {
84 | browserDetails._safariVersion = 13.0;
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/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 | // uses Safari Technology Preview.
49 | if (browsers.includes('Safari') && os.platform() === 'darwin' &&
50 | process.env.BVER === 'unstable' && !process.env.SAFARI_BIN) {
51 | process.env.SAFARI_BIN = '/Applications/Safari Technology Preview.app' +
52 | '/Contents/MacOS/Safari Technology Preview';
53 | }
54 |
55 | if (browsers.includes('firefox')) {
56 | const buildId = await download('firefox', process.env.BVER || 'stable',
57 | cacheDir, platform);
58 | process.env.FIREFOX_BIN = puppeteerBrowsers
59 | .computeExecutablePath({browser: 'firefox', buildId, cacheDir, platform});
60 | }
61 | if (browsers.includes('chrome')) {
62 | const buildId = await download('chrome', process.env.BVER || 'stable',
63 | cacheDir, platform);
64 | process.env.CHROME_BIN = puppeteerBrowsers
65 | .computeExecutablePath({browser: 'chrome', buildId, cacheDir, platform});
66 | }
67 |
68 | let chromeFlags = [
69 | '--use-fake-device-for-media-stream',
70 | '--use-fake-ui-for-media-stream',
71 | '--no-sandbox',
72 | '--headless', '--disable-gpu', '--remote-debugging-port=9222'
73 | ];
74 | if (process.env.CHROMEEXPERIMENT !== 'false') {
75 | chromeFlags.push('--enable-experimental-web-platform-features');
76 | }
77 |
78 | config.set({
79 | basePath: '..',
80 | frameworks: ['browserify', 'mocha', 'chai'],
81 | files: [
82 | 'dist/adapter_core5.js',
83 | 'test/getusermedia-mocha.js',
84 | 'test/e2e/*.js',
85 | ],
86 | exclude: [],
87 | preprocessors: {
88 | 'dist/adapter_core5.js': ['browserify']
89 | },
90 | reporters: ['mocha'],
91 | port: 9876,
92 | colors: true,
93 | logLevel: config.LOG_INFO,
94 | autoWatch: false,
95 | customLaunchers: {
96 | chrome: {
97 | base: 'Chrome',
98 | flags: chromeFlags
99 | },
100 | electron: {
101 | base: 'Electron',
102 | flags: ['--use-fake-device-for-media-stream']
103 | },
104 | firefox: {
105 | base: 'Firefox',
106 | prefs: {
107 | 'media.navigator.streams.fake': true,
108 | 'media.navigator.permission.disabled': true,
109 | },
110 | flags: ['-headless']
111 | }
112 | },
113 | singleRun: true,
114 | concurrency: Infinity,
115 | browsers,
116 | browserify: {
117 | debug: true,
118 | transform: ['brfs'],
119 | standalone: 'adapter',
120 | },
121 | });
122 | };
123 |
--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
1 | [](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/) and [Chai](http://chaijs.com/).
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](https://karma-runner.github.io/latest/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) and [Safari](https://www.npmjs.com/package/karma-safari-launcher).
11 |
12 | ## Development ##
13 | 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).
14 |
15 | This guide assumes you are running a Debian based Linux distribution (travis-multirunner currently fetches .deb browser packages).
16 |
17 | #### Clone the repo in desired folder
18 | ```bash
19 | git clone https://github.com/webrtc/adapter.git
20 | ```
21 |
22 | #### Install npm dependencies
23 | ```bash
24 | npm install
25 | ```
26 |
27 | #### Build
28 | In order to get a usable file, you need to build it.
29 | ```bash
30 | grunt build
31 | ```
32 | This will result in 2 files in the out/ folder:
33 | * adapter.js - includes all the shims and is visible in the browser under the global `adapter` object (window.adapter).
34 | * 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).
35 |
36 | #### Run tests
37 | Runs grunt and tests in test/tests.js. Change the browser to your choice, more details [here](#changeBrowser)
38 | ```bash
39 | BROWSER=chrome BVER=stable npm test
40 | ```
41 |
42 | #### Add tests
43 | When adding tests make sure to update the test expectation file for all browsers and supported version.
44 | The easiest way to do so is to set the `CI` and `UPDATE_STABILITYREPORTER` environment variables and
45 | re-run the tests with all browsers.
46 |
47 | Once your test is ready, create a pull request and see how it runs on travis-multirunner.
48 | Usually the expectation is for a test to pass in at least one browser. File browser bugs
49 | for tests that do not meet this expectation!
50 |
51 | #### Change browser and channel/version for testing
52 | Chrome stable is currently installed as the default browser for the tests.
53 |
54 | Currently Chrome and Firefox are supported[*](#expBrowser), check [travis-multirunner](https://github.com/DamonOehlman/travis-multirunner/blob/master/) repo for updates around this.
55 | Firefox channels supported are stable, beta, nightly and ESR.
56 | Chrome channels supported on Linux are stable, beta and unstable.
57 | Microsoft Edge is supported on Windows and Safari on OSX.
58 |
59 | 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.
60 | ```bash
61 | export BROWSER=firefox BVER=nightly
62 | ```
63 |
64 | Alternatively you can also do it without changing environment variables.
65 | ```bash
66 | BROWSER=firefox BVER=nightly npm test
67 | ```
68 |
69 | ### Getting crash dumps from karma
70 | Sometimes Chrome may crash when running the tests. This typically shows up in headless runs as a disconnect:
71 | ```
72 | 05 01 2018 10:42:14.225:WARN [HeadlessChrome 0.0.0 (Linux 0.0.0)]: Disconnected (1 times)
73 | ```
74 |
75 | Follow these steps to get a crash dump:
76 | * add a `browsers = [];` line in test/karma.conf.js to stop karma from starting Chrome
77 | * change `singlerun` to `false` in test/karma.conf.js
78 | * run `node_modules/.bin/karma start test/karma.conf.js` in a terminal to start a karma server
79 | * start Chrome with `google-chrome --use-fake-device-for-media-stream --use-fake-ui-for-media-stream http://localhost:9876`
80 | * run `node_modules/.bin/karma run test/karma.conf.js` to start the karma run
81 | * wait for the "awww snap" :-)
82 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | * after publishing a hotfiix use `npm dist-tag` to ensure latest still points to the highest version.
81 |
--------------------------------------------------------------------------------
/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', function() {
118 | if (window.adapter.browserDetails.browser === 'firefox') {
119 | this.skip();
120 | }
121 | const senders = pc.getSenders();
122 | senders.forEach(sender => pc.removeTrack(sender));
123 | expect(pc.getLocalStreams()).to.have.length(0);
124 | });
125 | });
126 | });
127 | });
128 | });
129 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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('Webkit 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 | describe('Safari regular expression', () => {
148 | const expr = /Version\/(\d+(\.?\d+))/;
149 | it('extracts the Safari version', () => {
150 | ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) ' +
151 | 'AppleWebKit/604.1.6 (KHTML, like Gecko) Version/10.2 Safari/604.1.6';
152 | expect(extractVersion(ua, expr, 1)).toBe(10.2);
153 | });
154 | });
155 | });
156 |
157 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 && parseFloat(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 = parseInt(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 = parseInt(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 = parseInt(extractVersion(navigator.userAgent,
191 | /AppleWebKit\/(\d+)\./, 1));
192 | result.supportsUnifiedPlan = window.RTCRtpTransceiver &&
193 | 'currentDirection' in window.RTCRtpTransceiver.prototype;
194 | // Only for internal usage.
195 | result._safariVersion = extractVersion(navigator.userAgent,
196 | /Version\/(\d+(\.?\d+))/, 1);
197 | } else { // Default fallthrough: not supported.
198 | result.browser = 'Not a supported browser.';
199 | return result;
200 | }
201 |
202 | return result;
203 | }
204 |
205 | /**
206 | * Checks if something is an object.
207 | *
208 | * @param {*} val The something you want to check.
209 | * @return true if val is an object, false otherwise.
210 | */
211 | function isObject(val) {
212 | return Object.prototype.toString.call(val) === '[object Object]';
213 | }
214 |
215 | /**
216 | * Remove all empty objects and undefined values
217 | * from a nested object -- an enhanced and vanilla version
218 | * of Lodash's `compact`.
219 | */
220 | export function compactObject(data) {
221 | if (!isObject(data)) {
222 | return data;
223 | }
224 |
225 | return Object.keys(data).reduce(function(accumulator, key) {
226 | const isObj = isObject(data[key]);
227 | const value = isObj ? compactObject(data[key]) : data[key];
228 | const isEmptyObject = isObj && !Object.keys(value).length;
229 | if (value === undefined || isEmptyObject) {
230 | return accumulator;
231 | }
232 | return Object.assign(accumulator, {[key]: value});
233 | }, {});
234 | }
235 |
236 | /* iterates the stats graph recursively. */
237 | export function walkStats(stats, base, resultSet) {
238 | if (!base || resultSet.has(base.id)) {
239 | return;
240 | }
241 | resultSet.set(base.id, base);
242 | Object.keys(base).forEach(name => {
243 | if (name.endsWith('Id')) {
244 | walkStats(stats, stats.get(base[name]), resultSet);
245 | } else if (name.endsWith('Ids')) {
246 | base[name].forEach(id => {
247 | walkStats(stats, stats.get(id), resultSet);
248 | });
249 | }
250 | });
251 | }
252 |
253 | /* filter getStats for a sender/receiver track. */
254 | export function filterStats(result, track, outbound) {
255 | const streamStatsType = outbound ? 'outbound-rtp' : 'inbound-rtp';
256 | const filteredResult = new Map();
257 | if (track === null) {
258 | return filteredResult;
259 | }
260 | const trackStats = [];
261 | result.forEach(value => {
262 | if (value.type === 'track' &&
263 | value.trackIdentifier === track.id) {
264 | trackStats.push(value);
265 | }
266 | });
267 | trackStats.forEach(trackStat => {
268 | result.forEach(stats => {
269 | if (stats.type === streamStatsType && stats.trackId === trackStat.id) {
270 | walkStats(result, stats, filteredResult);
271 | }
272 | });
273 | });
274 | return filteredResult;
275 | }
276 |
277 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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' &&
357 | browserDetails._safariVersion >= 13.1) {
358 | return;
359 | }
360 | const nativeSRD = window.RTCPeerConnection.prototype.setRemoteDescription;
361 | window.RTCPeerConnection.prototype.setRemoteDescription =
362 | function setRemoteDescription(desc) {
363 | if (desc && desc.sdp && desc.sdp.indexOf('\na=extmap-allow-mixed') !== -1) {
364 | const sdp = desc.sdp.split('\n').filter((line) => {
365 | return line.trim() !== 'a=extmap-allow-mixed';
366 | }).join('\n');
367 | // Safari enforces read-only-ness of RTCSessionDescription fields.
368 | if (window.RTCSessionDescription &&
369 | desc instanceof window.RTCSessionDescription) {
370 | arguments[0] = new window.RTCSessionDescription({
371 | type: desc.type,
372 | sdp,
373 | });
374 | } else {
375 | desc.sdp = sdp;
376 | }
377 | }
378 | return nativeSRD.apply(this, arguments);
379 | };
380 | }
381 |
382 | export function shimAddIceCandidateNullOrEmpty(window, browserDetails) {
383 | // Support for addIceCandidate(null or undefined)
384 | // as well as addIceCandidate({candidate: "", ...})
385 | // https://bugs.chromium.org/p/chromium/issues/detail?id=978582
386 | // Note: must be called before other polyfills which change the signature.
387 | if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) {
388 | return;
389 | }
390 | const nativeAddIceCandidate =
391 | window.RTCPeerConnection.prototype.addIceCandidate;
392 | if (!nativeAddIceCandidate || nativeAddIceCandidate.length === 0) {
393 | return;
394 | }
395 | window.RTCPeerConnection.prototype.addIceCandidate =
396 | function addIceCandidate() {
397 | if (!arguments[0]) {
398 | if (arguments[1]) {
399 | arguments[1].apply(null);
400 | }
401 | return Promise.resolve();
402 | }
403 | // Firefox 68+ emits and processes {candidate: "", ...}, ignore
404 | // in older versions.
405 | // Native support for ignoring exists for Chrome M77+.
406 | // Safari ignores as well, exact version unknown but works in the same
407 | // version that also ignores addIceCandidate(null).
408 | if (((browserDetails.browser === 'chrome' && browserDetails.version < 78)
409 | || (browserDetails.browser === 'firefox'
410 | && browserDetails.version < 68)
411 | || (browserDetails.browser === 'safari'))
412 | && arguments[0] && arguments[0].candidate === '') {
413 | return Promise.resolve();
414 | }
415 | return nativeAddIceCandidate.apply(this, arguments);
416 | };
417 | }
418 |
419 | // Note: Make sure to call this ahead of APIs that modify
420 | // setLocalDescription.length
421 | export function shimParameterlessSetLocalDescription(window, browserDetails) {
422 | if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) {
423 | return;
424 | }
425 | const nativeSetLocalDescription =
426 | window.RTCPeerConnection.prototype.setLocalDescription;
427 | if (!nativeSetLocalDescription || nativeSetLocalDescription.length === 0) {
428 | return;
429 | }
430 | window.RTCPeerConnection.prototype.setLocalDescription =
431 | function setLocalDescription() {
432 | let desc = arguments[0] || {};
433 | if (typeof desc !== 'object' || (desc.type && desc.sdp)) {
434 | return nativeSetLocalDescription.apply(this, arguments);
435 | }
436 | // The remaining steps should technically happen when SLD comes off the
437 | // RTCPeerConnection's operations chain (not ahead of going on it), but
438 | // this is too difficult to shim. Instead, this shim only covers the
439 | // common case where the operations chain is empty. This is imperfect, but
440 | // should cover many cases. Rationale: Even if we can't reduce the glare
441 | // window to zero on imperfect implementations, there's value in tapping
442 | // into the perfect negotiation pattern that several browsers support.
443 | desc = {type: desc.type, sdp: desc.sdp};
444 | if (!desc.type) {
445 | switch (this.signalingState) {
446 | case 'stable':
447 | case 'have-local-offer':
448 | case 'have-remote-pranswer':
449 | desc.type = 'offer';
450 | break;
451 | default:
452 | desc.type = 'answer';
453 | break;
454 | }
455 | }
456 | if (desc.sdp || (desc.type !== 'offer' && desc.type !== 'answer')) {
457 | return nativeSetLocalDescription.apply(this, [desc]);
458 | }
459 | const func = desc.type === 'offer' ? this.createOffer : this.createAnswer;
460 | return func.apply(this)
461 | .then(d => nativeSetLocalDescription.apply(this, [d]));
462 | };
463 | }
464 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------