Repository: webrtc/samples Branch: gh-pages Commit: 96ded9705722 Files: 241 Total size: 940.7 KB Directory structure: gitextract_t5d8f4yo/ ├── .eslintrc.js ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── interop-tests.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── .stylelintrc ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── google1b7eb21c5b594ba0.html ├── index.html ├── package.json ├── src/ │ ├── content/ │ │ ├── capture/ │ │ │ ├── canvas-filter/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── canvas-pc/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── canvas-record/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── canvas-video/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── video-contenthint/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── video-pc/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── video-video/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ └── worker-process/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── worker.js │ │ ├── datachannel/ │ │ │ ├── basic/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ ├── main.js │ │ │ │ └── test.js │ │ │ ├── channel/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ ├── main.js │ │ │ │ └── test.js │ │ │ ├── datatransfer/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ ├── main.js │ │ │ │ └── test.js │ │ │ ├── filetransfer/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ ├── main.js │ │ │ │ └── test.js │ │ │ └── messaging/ │ │ │ ├── index.html │ │ │ ├── main.css │ │ │ └── main.js │ │ ├── devices/ │ │ │ ├── input-output/ │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ ├── main.js │ │ │ │ └── test.js │ │ │ └── multi/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ ├── js/ │ │ │ │ └── main.js │ │ │ └── video/ │ │ │ ├── chrome.ogv │ │ │ └── chrome.webm │ │ ├── extensions/ │ │ │ ├── multipleroutes/ │ │ │ │ └── src/ │ │ │ │ ├── README.md │ │ │ │ ├── _locales/ │ │ │ │ │ └── en/ │ │ │ │ │ └── messages.json │ │ │ │ ├── manifest.json │ │ │ │ ├── options.html │ │ │ │ └── options.js │ │ │ └── svc/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ └── main.js │ │ ├── getusermedia/ │ │ │ ├── audio/ │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── canvas/ │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── exposure/ │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── filter/ │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── getdisplaymedia/ │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── gum/ │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ ├── main.js │ │ │ │ └── test.js │ │ │ ├── pan-tilt-zoom/ │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── record/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── resolution/ │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ ├── main.js │ │ │ │ └── test.js │ │ │ ├── source/ │ │ │ │ └── index.html │ │ │ └── volume/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ ├── soundmeter.js │ │ │ └── volume-meter-processor.js │ │ ├── insertable-streams/ │ │ │ ├── audio-processing/ │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ ├── main.js │ │ │ │ └── worker.js │ │ │ ├── endtoend-encryption/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ ├── main.js │ │ │ │ ├── test.js │ │ │ │ ├── videopipe.js │ │ │ │ └── worker.js │ │ │ ├── video/ │ │ │ │ └── index.html │ │ │ ├── video-analyzer/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ ├── video-crop/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ ├── main.js │ │ │ │ └── worker.js │ │ │ ├── video-processing/ │ │ │ │ ├── css/ │ │ │ │ │ └── main.css │ │ │ │ ├── index.html │ │ │ │ └── js/ │ │ │ │ ├── camera-source.js │ │ │ │ ├── canvas-source.js │ │ │ │ ├── canvas-transform.js │ │ │ │ ├── main.js │ │ │ │ ├── peer-connection-pipe.js │ │ │ │ ├── peer-connection-sink.js │ │ │ │ ├── peer-connection-source.js │ │ │ │ ├── pipeline.js │ │ │ │ ├── simple-transforms.js │ │ │ │ ├── video-mirror-helper.js │ │ │ │ ├── video-sink.js │ │ │ │ ├── video-source.js │ │ │ │ ├── webcodec-transform.js │ │ │ │ └── webgl-transform.js │ │ │ └── webgpu/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ ├── multi_video_main.js │ │ │ ├── multi_video_worker.js │ │ │ └── multi_video_worker_manager.js │ │ └── peerconnection/ │ │ ├── always-negotiate-datachannels/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ └── main.js │ │ ├── audio/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── test.js │ │ ├── bandwidth/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ └── main.js │ │ ├── change-codecs/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── test.js │ │ ├── channel/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── test.js │ │ ├── constraints/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ └── main.js │ │ ├── create-offer/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ └── main.js │ │ ├── dtmf/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── test.js │ │ ├── endtoend-encryption/ │ │ │ └── index.html │ │ ├── multiple/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── test.js │ │ ├── multiple-relay/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ └── main.js │ │ ├── munge-sdp/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── test.js │ │ ├── negotiate-timing/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── test.js │ │ ├── pc1/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── test.js │ │ ├── per-frame-callback/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ └── main.js │ │ ├── perfect-negotiation/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── peer.js │ │ ├── pr-answer/ │ │ │ ├── index.html │ │ │ └── js/ │ │ │ └── main.js │ │ ├── restart-ice/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── test.js │ │ ├── states/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── test.js │ │ ├── trickle-ice/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── test.js │ │ ├── upgrade/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── test.js │ │ ├── video-analyzer/ │ │ │ └── index.html │ │ ├── webaudio-input/ │ │ │ ├── css/ │ │ │ │ └── main.css │ │ │ ├── index.html │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── webaudioextended.js │ │ └── webaudio-output/ │ │ ├── css/ │ │ │ └── main.css │ │ ├── index.html │ │ └── js/ │ │ └── main.js │ ├── css/ │ │ └── main.css │ ├── js/ │ │ ├── lib/ │ │ │ └── ga.js │ │ ├── third_party/ │ │ │ ├── graph.js │ │ │ ├── streamvisualizer.js │ │ │ └── webgl_teapot/ │ │ │ ├── cameracontroller.js │ │ │ ├── demo.js │ │ │ ├── matrix4x4.js │ │ │ ├── teapot-streams.js │ │ │ ├── webgl-debug.js │ │ │ └── webgl-utils.js │ │ └── videopipe.js │ └── video/ │ ├── chrome.webm │ └── mixed-content.webm └── test/ ├── download-browsers.js ├── interop/ │ └── connection.test.js ├── steps.js ├── webdriver.js └── webrtcclient.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.js ================================================ module.exports = { 'extends': 'google', 'parserOptions': { 'ecmaVersion': 2017, 'sourceType': 'module', }, 'env': { 'browser': true, 'es6': true, 'node': true, 'jest': true }, 'rules': { 'max-len': 'off', 'require-jsdoc': 'off', 'arrow-parens': 'off', 'comma-dangle': 'off', 'no-throw-literal': 'off', 'camelcase': 'off', 'prefer-rest-params': 'off', 'no-invalid-this': 'off', 'eol-last': 'off', 'no-undef': 2, }, "globals": { "adapter": true, "browserSupportsIPHandlingPolicy": true, "browserSupportsNonProxiedUdpBoolean": true, "chrome": true, "ga": true, "getPolicyFromBooleans": true, "importScripts": true, // From WebGPU specification "GPUBufferUsage": true, "GPUTextureUsage": true, // From Streams specification "TransformStream": true, // From WebCodec specification "AudioData": true, "AudioEncoder": true, "AudioDecoder": true, "VideoFrame": true, "VideoEncoder": true, "VideoDecoder": true, }, "plugins": ["jest"] }; ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- # Please read first! Please use [discuss-webrtc](https://groups.google.com/forum/#!forum/discuss-webrtc) for general technical discussions and questions. If you have found an issue/bug with the native `libwebrtc` SDK or a browser's behaviour around WebRTC please create an issue in the relevant bug tracker. You can find more information on how to submit a bug and do so in the right place [here](https://webrtc.googlesource.com/src/+/refs/heads/main/docs/bug-reporting.md) - [ ] I understand that issues created here are _only_ relevant to the samples in this repo - not browser or SDK bugs - [ ] I have provided steps to reproduce - [ ] I have provided browser name and version - [ ] I have provided a link to the sample here or a modified version thereof **Note: If the checkboxes above are not checked (which you do after the issue is posted), the issue will be closed.** ## Browser affected **Browser name including version (e.g. Chrome 64.0.3282.119)** ## Description ## Steps to reproduce ## Expected results ## Actual results ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Please use the discuss-webrtc mailing list for general questions url: https://groups.google.com/g/discuss-webrtc ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ **Description** **Purpose** ================================================ FILE: .github/workflows/interop-tests.yml ================================================ on: schedule: - cron: "30 5 * * *" jobs: interop: runs-on: ubuntu-22.04 timeout-minutes: 5 strategy: fail-fast: false matrix: browserA: [chrome, firefox] browserB: [firefox, chrome] bver: [unstable] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - run: npm install - run: sudo rm /usr/bin/chromedriver /usr/bin/geckodriver # remove preinstalled github chromedriver/geckodriver from $PATH - run: Xvfb :99 & - run: BROWSER_A=${{matrix.browserA}} BROWSER_B=${{matrix.browserB}} BVER=${{matrix.bver}} DISPLAY=:99.0 node test/download-browsers.js - run: BROWSER_A=${{matrix.browserA}} BROWSER_B=${{matrix.browserB}} BVER=${{matrix.bver}} DISPLAY=:99.0 node_modules/.bin/jest --retries=3 test/interop/ ================================================ FILE: .github/workflows/test.yml ================================================ name: lint-and-test on: [pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - run: npm install - run: npm run eslint - run: npm run stylelint test: needs: lint runs-on: ubuntu-22.04 timeout-minutes: 5 strategy: matrix: browser: [chrome] version: [stable] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - run: npm install - run: sudo rm /usr/bin/chromedriver # remove preinstalled github chromedriver from $PATH - run: Xvfb :99 & - run: BROWSER=${{matrix.browser}} BVER=${{matrix.version}} DISPLAY=:99.0 npm run jest -- --retries=3 ================================================ FILE: .gitignore ================================================ browsers* .eslintcache firefox-*.tar.bz2 node_modules .DS_Store validation-report.json validation-status.json .idea firefox_profile/* tests_output *.log *~ \#*# ================================================ FILE: .npmrc ================================================ package-lock=false ================================================ FILE: .stylelintrc ================================================ { "extends": "stylelint-config-recommended" } ================================================ FILE: AUTHORS ================================================ The WebRTC Project Authors The Chromium Authors ================================================ FILE: CONTRIBUTING.md ================================================ # WebRTC welcomes patches/pulls for features and bug fixes! For contributors external to Google, follow the instructions given in the [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual). 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. If you plan to add a new sample or make significant changes to an existing sample, we recommend that you start by creating a [new issue](https://github.com/webrtc/samples/issues/new) where we can discuss the details. # How to start developing a patch, new feature or bug fix ## Clone the repo in desired folder ```bash git clone https://github.com/webrtc/samples.git ``` ## Install npm dependencies ```bash npm install ``` ## Start web server for development ```bash npm start ``` ================================================ FILE: LICENSE.md ================================================ Copyright (c) 2014, The WebRTC project authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # WebRTC Code Samples This is a repository for the WebRTC JavaScript code samples. All of the samples can be tested from [webrtc.github.io/samples](https://webrtc.github.io/samples). To run the samples locally ``` npm install && npm start ``` and open your browser on the page indicated. ## Contributing We welcome contributions and bugfixes. Please see [CONTRIBUTING.md](https://github.com/webrtc/samples/blob/gh-pages/CONTRIBUTING.md) for details. ## Bugs If you encounter a bug or problem with one of the samples, please submit a [new issue](https://github.com/webrtc/samples/issues/new) so we know about it and can fix it. Please avoid submitting issues on this repository for general problems you have with WebRTC. If you have found a bug in the WebRTC APIs, please see [webrtc.org/bugs](https://webrtc.org/support/bug-reporting) for how to submit bugs on the affected browsers. If you need support on how to implement your own WebRTC-based application, please see the [discuss-webrtc](https://groups.google.com/forum/#!forum/discuss-webrtc) Google Group. ================================================ FILE: google1b7eb21c5b594ba0.html ================================================ google-site-verification: google1b7eb21c5b594ba0.html ================================================ FILE: index.html ================================================ WebRTC samples

WebRTC samples

This is a collection of small samples demonstrating various parts of the WebRTC APIs. The code for all samples are available in the GitHub repository.

Most of the samples use adapter.js, a shim to insulate apps from spec changes and prefix differences.

https://webrtc.org/getting-started/testing lists command line flags useful for development and testing with Chrome.

Patches and issues welcome! See CONTRIBUTING.md for instructions.

Warning: It is highly recommended to use headphones when testing these samples, as it will otherwise risk loud audio feedback on your system.

getUserMedia():

Access media devices

Devices:

Query media devices

Stream capture:

Stream from canvas or video elements

RTCPeerConnection:

Controlling peer connectivity

RTCDataChannel:

Send arbitrary data over peer connections

Video chat:

Full featured WebRTC application

Insertable Streams:

API for processing media

================================================ FILE: package.json ================================================ { "name": "webrtc-samples", "private": true, "version": "1.0.0", "description": "Project checking for WebRTC GitHub samples repo", "keywords": [ "webrtc" ], "homepage": "https://webrtc.github.io/samples/", "bugs": { "url": "https://github.com/webrtc/samples/issues" }, "license": "BSD-3-Clause", "author": "The WebRTC project authors", "repository": { "type": "git", "url": "https://github.com/webrtc/samples.git" }, "scripts": { "start": "http-server . -c-1 -a 127.0.0.1", "test": "npm run eslint && npm run stylelint", "eslint": "eslint 'test/**.js' 'src/content/**/*.js'", "jest": "node test/download-browsers.js && jest --testTimeout 5000 --maxWorkers=1 src/content/", "stylelint": "stylelint 'src/**/*.css'" }, "eslintIgnore": [ "'**/third_party/*.js'" ], "devDependencies": { "@puppeteer/browsers": "^2.2.0", "eslint": "^8.9.0", "eslint-config-google": "^0.14.0", "eslint-plugin-jest": "^27.4.0", "http-server": "^14.1.0", "jest": "^29.7.0", "selenium-webdriver": "^4.19.0", "stylelint": "^14.5.3", "stylelint-config-recommended": "^7.0.0" } } ================================================ FILE: src/content/capture/canvas-filter/css/main.css ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ canvas { background-color: #ccc; --width: calc(45%); width: var(--width); height: calc(var(--width) * 0.75); margin: 1em; vertical-align: top; } video { --width: calc(45%); width: var(--width); height: calc(var(--width) * 0.75); margin: 1em; object-fit: cover; } ================================================ FILE: src/content/capture/canvas-filter/index.html ================================================ Video to Canvas to video

WebRTC samples Stream camera via a canvas to a video element

The camera is captured to a video element, which is mapped onto a canvas, and a red square is added.

The canvas is then captured to an ImageData object, and painted onto a second canvas.

A stream is captured from the second canvas element using its captureStream() method and set as the srcObject of the video element.

The inputStream, source, canvasIn, canvasOut, result, and stream variables are in global scope, so you can inspect them from the browser console.

View source on GitHub
================================================ FILE: src/content/capture/canvas-filter/js/main.js ================================================ /* * Copyright (c) 2019 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const source = document.querySelector('#source'); // TODO(hta): Use OffscreenCanvas for the intermediate canvases. const canvasIn = document.querySelector('#canvas-source'); const canvasOut = document.querySelector('#canvas-result'); const result = document.querySelector('#result'); const stream = canvasOut.captureStream(); let inputStream = null; let imageData = null; result.srcObject = stream; function loop() { if (source.videoWidth > 0 && source.videoHeight > 0) { canvasIn.width = source.videoWidth; canvasIn.height = source.videoHeight; const ctx = canvasIn.getContext('2d'); ctx.drawImage(source, 0, 0); // Put a red square into the image, to mark it as "processed". ctx.fillStyle = '#FF0000'; ctx.fillRect(10, 10, 80, 80); imageData = ctx.getImageData(0, 0, canvasIn.width, canvasIn.height); // At this point, we have data that can be transferred. // We paint it on the second canvas. canvasOut.width = source.videoWidth; canvasOut.height = source.videoHeight; const outCtx = canvasOut.getContext('2d'); outCtx.putImageData(imageData, 0, 0); } window.requestAnimationFrame(loop); } (async () => { inputStream = await navigator.mediaDevices.getUserMedia({video: true}); source.srcObject = inputStream; source.play(); result.play(); window.requestAnimationFrame(loop); })(); ================================================ FILE: src/content/capture/canvas-pc/css/main.css ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ canvas { background-color: #ccc; --width: calc(45%); width: var(--width); height: calc(var(--width) * 0.75); margin: 1em; vertical-align: top; } video { --width: calc(45%); width: var(--width); height: calc(var(--width) * 0.75); margin: 1em; object-fit: cover; } #autoplay { display: none; } ================================================ FILE: src/content/capture/canvas-pc/index.html ================================================ Canvas to peer connection

WebRTC samples Stream from canvas to peer connection

Due to autoplay policy the video seems not to be playing. Clicking the left teapot usually resolves this.

Click and drag on the canvas element (on the left) to move the teapot.

This demo requires Firefox 47 or Chrome 52 (or later).

The teapot is drawn on the canvas element using WebGL. A stream is captured from the canvas using its captureStream() method and streamed via a peer connection to the video element on the right.

View the browser console to see logging.

Several variables are in global scope, so you can inspect them from the console: canvas, video, pc1, pc2 and stream.

For more demos and information about captureStream(), see Media Capture from Canvas Implementation.

For more information about RTCPeerConnection, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/capture/canvas-pc/js/main.js ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* global main */ 'use strict'; const canvas = document.querySelector('canvas'); const video = document.querySelector('video'); let pc1; let pc2; let startTime; video.addEventListener('loadedmetadata', function() { document.getElementById('autoplay').style.display = 'none'; console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); video.onresize = () => { console.log(`Remote video size changed to ${video.videoWidth}x${video.videoHeight}`); // We'll use the first onsize callback as an indication that video has started // playing out. if (startTime) { const elapsedTime = window.performance.now() - startTime; console.log(`Setup time: ${elapsedTime.toFixed(3)}ms`); startTime = null; } }; // Call main() in demo.js main(); const stream = canvas.captureStream(); console.log('Got stream from canvas'); call(); function call() { console.log('Starting call'); startTime = window.performance.now(); const videoTracks = stream.getVideoTracks(); const audioTracks = stream.getAudioTracks(); if (videoTracks.length > 0) { console.log(`Using video device: ${videoTracks[0].label}`); } if (audioTracks.length > 0) { console.log(`Using audio device: ${audioTracks[0].label}`); } const servers = null; pc1 = new RTCPeerConnection(servers); console.log('Created local peer connection object pc1'); pc1.onicecandidate = e => onIceCandidate(pc1, e); pc2 = new RTCPeerConnection(servers); console.log('Created remote peer connection object pc2'); pc2.onicecandidate = e => onIceCandidate(pc2, e); pc1.oniceconnectionstatechange = e => onIceStateChange(pc1, e); pc2.oniceconnectionstatechange = e => onIceStateChange(pc2, e); pc2.ontrack = gotRemoteStream; stream.getTracks().forEach( track => { pc1.addTrack( track, stream ); } ); console.log('Added local stream to pc1'); console.log('pc1 createOffer start'); pc1.createOffer(onCreateOfferSuccess, onCreateSessionDescriptionError); } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); } function onCreateOfferSuccess(desc) { console.log(`Offer from pc1\n${desc.sdp}`); console.log('pc1 setLocalDescription start'); pc1.setLocalDescription(desc, () => onSetLocalSuccess(pc1), onSetSessionDescriptionError); console.log('pc2 setRemoteDescription start'); pc2.setRemoteDescription(desc, () => onSetRemoteSuccess(pc2), onSetSessionDescriptionError); console.log('pc2 createAnswer start'); // Since the 'remote' side has no media stream we need // to pass in the right constraints in order for it to // accept the incoming offer of audio and video. pc2.createAnswer(onCreateAnswerSuccess, onCreateSessionDescriptionError); } function onSetLocalSuccess(pc) { console.log(`${getName(pc)} setLocalDescription complete`); } function onSetRemoteSuccess(pc) { console.log(`${getName(pc)} setRemoteDescription complete`); } function onSetSessionDescriptionError(error) { console.log(`Failed to set session description: ${error.toString()}`); } function gotRemoteStream(e) { if (video.srcObject !== e.streams[0]) { video.srcObject = e.streams[0]; console.log('pc2 received remote stream'); if (video.paused) { document.getElementById('autoplay').style.display = 'block'; } } } function onCreateAnswerSuccess(desc) { console.log(`Answer from pc2:\n${desc.sdp}`); console.log('pc2 setLocalDescription start'); pc2.setLocalDescription(desc, () => onSetLocalSuccess(pc2), onSetSessionDescriptionError); console.log('pc1 setRemoteDescription start'); pc1.setRemoteDescription(desc, () => onSetRemoteSuccess(pc1), onSetSessionDescriptionError); } function onIceCandidate(pc, event) { getOtherPc(pc).addIceCandidate(event.candidate) .then( () => onAddIceCandidateSuccess(pc), err => onAddIceCandidateError(pc, err) ); console.log(`${getName(pc)} ICE candidate: ${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess(pc) { console.log(`${getName(pc)} addIceCandidate success`); } function onAddIceCandidateError(pc, error) { console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`); } function onIceStateChange(pc, event) { if (pc) { console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`); console.log('ICE state change event: ', event); } } function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } ================================================ FILE: src/content/capture/canvas-record/css/main.css ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 3px 10px 0; padding-left: 2px; padding-right: 2px; width: 99px; } button:last-of-type { margin: 0; } p.borderBelow { margin: 0 0 20px 0; padding: 0 0 20px 0; } canvas { background-color: #ccc; --width: calc(45%); width: var(--width); height: calc(var(--width) * 0.75); margin: 1em; vertical-align: top; } video { --width: calc(45%); width: var(--width); height: calc(var(--width) * 0.75); margin: 1em; object-fit: cover; } ================================================ FILE: src/content/capture/canvas-record/index.html ================================================ Record canvas stream

WebRTC samples Record stream from a canvas

Click and drag on the canvas (on the left) to move the teapot.

This demo requires Firefox 43 or above, Chrome 51 or above, or Chrome 50 with Experimental Web Platform features enabled.

The teapot is drawn on the canvas element using WebGL. A stream is captured from the canvas element using its captureStream() method then recorded using the MediaRecorder API.

The canvas, video, and stream variables are in global scope, so you can inspect them from the browser console.

For more demos and information about captureStream(), see Media Capture from Canvas Implementation.

For more information see the MediaStream Recording API Editor's Draft.

View source on GitHub
================================================ FILE: src/content/capture/canvas-record/js/main.js ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /* globals main */ // This code is adapted from // https://rawgit.com/Miguelao/demos/master/mediarecorder.html /* globals main, MediaRecorder */ const mediaSource = new MediaSource(); mediaSource.addEventListener('sourceopen', handleSourceOpen, false); let mediaRecorder; let recordedBlobs; let sourceBuffer; const canvas = document.querySelector('canvas'); const video = document.querySelector('video'); const recordButton = document.querySelector('button#record'); const playButton = document.querySelector('button#play'); const downloadButton = document.querySelector('button#download'); recordButton.onclick = toggleRecording; playButton.onclick = play; downloadButton.onclick = download; // Start the GL teapot on the canvas main(); const stream = canvas.captureStream(); // frames per second console.log('Started stream capture from canvas element: ', stream); function handleSourceOpen(event) { console.log('MediaSource opened'); sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp8"'); console.log('Source buffer: ', sourceBuffer); } function handleDataAvailable(event) { if (event.data && event.data.size > 0) { recordedBlobs.push(event.data); } } function handleStop(event) { console.log('Recorder stopped: ', event); const superBuffer = new Blob(recordedBlobs, {type: 'video/webm'}); video.src = window.URL.createObjectURL(superBuffer); } function toggleRecording() { if (recordButton.textContent === 'Start Recording') { startRecording(); } else { stopRecording(); recordButton.textContent = 'Start Recording'; playButton.disabled = false; downloadButton.disabled = false; } } // The nested try blocks will be simplified when Chrome 47 moves to Stable function startRecording() { let options = {mimeType: 'video/webm'}; recordedBlobs = []; try { mediaRecorder = new MediaRecorder(stream, options); } catch (e0) { console.log('Unable to create MediaRecorder with options Object: ', e0); try { options = {mimeType: 'video/webm,codecs=vp9'}; mediaRecorder = new MediaRecorder(stream, options); } catch (e1) { console.log('Unable to create MediaRecorder with options Object: ', e1); try { options = 'video/vp8'; // Chrome 47 mediaRecorder = new MediaRecorder(stream, options); } catch (e2) { alert('MediaRecorder is not supported by this browser.\n\n' + 'Try Firefox 29 or later, or Chrome 47 or later, ' + 'with Enable experimental Web Platform features enabled from chrome://flags.'); console.error('Exception while creating MediaRecorder:', e2); return; } } } console.log('Created MediaRecorder', mediaRecorder, 'with options', options); recordButton.textContent = 'Stop Recording'; playButton.disabled = true; downloadButton.disabled = true; mediaRecorder.onstop = handleStop; mediaRecorder.ondataavailable = handleDataAvailable; mediaRecorder.start(100); // collect 100ms of data console.log('MediaRecorder started', mediaRecorder); } function stopRecording() { mediaRecorder.stop(); console.log('Recorded Blobs: ', recordedBlobs); video.controls = true; } function play() { video.play(); } function download() { const blob = new Blob(recordedBlobs, {type: 'video/webm'}); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = 'test.webm'; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 100); } ================================================ FILE: src/content/capture/canvas-video/css/main.css ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ canvas { background-color: #ccc; --width: calc(45%); width: var(--width); height: calc(var(--width) * 0.75); margin: 1em; vertical-align: top; } video { --width: calc(45%); width: var(--width); height: calc(var(--width) * 0.75); margin: 1em; object-fit: cover; } ================================================ FILE: src/content/capture/canvas-video/index.html ================================================ Canvas to video

WebRTC samples Stream from canvas to video element

Click and drag on the canvas (on the left) to move the teapot.

This demo requires Firefox 47 or Chrome 52 (or later).

The teapot is drawn on the canvas element using WebGL. A stream is captured from the canvas element using its captureStream() method and set as the srcObject of the video element.

The canvas, video, and stream variables are in global scope, so you can inspect them from the browser console.

For more demos and information about captureStream(), see Media Capture from Canvas Implementation.

View source on GitHub
================================================ FILE: src/content/capture/canvas-video/js/main.js ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* global main */ 'use strict'; // Call main() in demo.js main(); const canvas = document.querySelector('canvas'); const video = document.querySelector('video'); const stream = canvas.captureStream(); video.srcObject = stream; ================================================ FILE: src/content/capture/video-contenthint/css/main.css ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ video { --width: calc(45%); width: var(--width); height: calc(var(--width) * 9 / 16); margin: 1em; object-fit: cover; } .video-container { border-bottom: 1px solid grey; font-style: italic; margin: 20px; } #videos { text-align: center; width: 100%; } ================================================ FILE: src/content/capture/video-contenthint/index.html ================================================ MediaStreamTrack Content Hints

WebRTC samples Guiding video encoding with content hints

Source video file (high bitrate)

"motion" video @ 50kbps

"detail" video @ 50kbps

This demo requires Chrome 57.0.2957.0 or later with Experimental Web Platform features enabled from chrome://flags.

A stream is captured from the source video using the captureStream() method. The stream is cloned and transmitted via two separate PeerConnections using 50kbps of video bandwidth. This is insufficient to generate good quality in the encoded bitstream, so trade-offs have to be made.

The transmitted stream tracks are using MediaStreamTrack Content Hints to indicate characteristics in the video stream, which informs PeerConnection on how to encode the track (to prefer motion or individual frame detail).

The text part of the clip shows a clear case for when 'detail' is better, and the fighting scene shows a clear case for when 'motion' is better. The spinning model however shows a case where 'motion' or 'detail' are not clear-cut decisions and even with good content detection what's preferred depends on what the user prefers.

Other MediaStreamTrack consumers such as MediaStreamRecorder can also make use of this information to guide encoding parameters for the stream without additional extensions to the MediaStreamRecorder specification, but this is currently not implemented in Chromium.

View source on GitHub
================================================ FILE: src/content/capture/video-contenthint/js/main.js ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const srcVideo = document.getElementById('srcVideo'); const motionVideo = document.getElementById('motionVideo'); const detailVideo = document.getElementById('detailVideo'); let srcStream; let motionStream; let detailStream; const offerOptions = { offerToReceiveAudio: 0, offerToReceiveVideo: 1 }; function maybeCreateStream() { if (srcStream) { return; } if (srcVideo.captureStream) { srcStream = srcVideo.captureStream(); call(); } else { console.log('captureStream() not supported'); } } // Video tag capture must be set up after video tracks are enumerated. srcVideo.oncanplay = maybeCreateStream; if (srcVideo.readyState >= 3) { // HAVE_FUTURE_DATA // Video is already ready to play, call maybeCreateStream in case oncanplay // fired before we registered the event handler. maybeCreateStream(); } srcVideo.play(); function setVideoTrackContentHints(stream, hint) { const tracks = stream.getVideoTracks(); tracks.forEach(track => { if ('contentHint' in track) { track.contentHint = hint; if (track.contentHint !== hint) { console.log('Invalid video track contentHint: \'' + hint + '\''); } } else { console.log('MediaStreamTrack contentHint attribute not supported'); } }); } function call() { // This creates multiple independent PeerConnections instead of multiple // streams on a single PeerConnection object so that b=AS (the bitrate // constraints) can be applied independently. motionStream = srcStream.clone(); // TODO(pbos): Remove fluid when no clients use it, motion is the newer name. setVideoTrackContentHints(motionStream, 'fluid'); setVideoTrackContentHints(motionStream, 'motion'); establishPC(motionVideo, motionStream); detailStream = srcStream.clone(); // TODO(pbos): Remove detailed when no clients use it, detail is the newer // name. setVideoTrackContentHints(detailStream, 'detailed'); setVideoTrackContentHints(detailStream, 'detail'); establishPC(detailVideo, detailStream); } function establishPC(videoTag, stream) { const pc1 = new RTCPeerConnection(null); const pc2 = new RTCPeerConnection(null); pc1.onicecandidate = e => { onIceCandidate(pc1, pc2, e); }; pc2.onicecandidate = e => { onIceCandidate(pc2, pc1, e); }; pc2.ontrack = event => { if (videoTag.srcObject !== event.streams[0]) { videoTag.srcObject = event.streams[0]; } }; stream.getTracks().forEach(track => pc1.addTrack(track, stream)); pc1.createOffer(offerOptions) .then(desc => { pc1.setLocalDescription(desc) .then(() => pc2.setRemoteDescription(desc)) .then(() => pc2.createAnswer()) .then(answerDesc => onCreateAnswerSuccess(pc1, pc2, answerDesc)) .catch(onSetSessionDescriptionError); }) .catch(e => console.log('Failed to create session description: ' + e.toString())); } function onSetSessionDescriptionError(error) { console.log('Failed to set session description: ' + error.toString()); } function onCreateAnswerSuccess(pc1, pc2, desc) { // Hard-code video bitrate to 50kbps. desc.sdp = desc.sdp.replace(/a=mid:(.*)\r\n/g, 'a=mid:$1\r\nb=AS:' + 50 + '\r\n'); pc2.setLocalDescription(desc) .then(() => pc1.setRemoteDescription(desc)) .catch(onSetSessionDescriptionError); } function onIceCandidate(pc, otherPc, event) { otherPc.addIceCandidate(event.candidate); } ================================================ FILE: src/content/capture/video-pc/css/main.css ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ video { margin: 0 10px 0 0; width: calc(50% - 7px); } video:last-of-type { margin-right: 0; } @media screen and (max-width: 400px) { video { margin: 0 5px 20px 0; width: calc(50% - 5px); } } ================================================ FILE: src/content/capture/video-pc/index.html ================================================ Video to peer connection

WebRTC samples Stream from a video to a peer connection

This demo requires Firefox 47, Chrome 53 with Experimental Web Platform features enabled from chrome://flags.

A stream is captured from the video on the left using the captureStream() method, and streamed via a peer connection to the video element on the right.

View the browser console to see logging.

Several variables are in global scope, so you can inspect them from the console: pc1, pc2 and stream.

For more information about RTCPeerConnection, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/capture/video-pc/js/main.js ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const leftVideo = document.getElementById('leftVideo'); const rightVideo = document.getElementById('rightVideo'); let stream; let pc1; let pc2; const offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 1 }; let startTime; function maybeCreateStream() { if (stream) { return; } if (leftVideo.captureStream) { stream = leftVideo.captureStream(); console.log('Captured stream from leftVideo with captureStream', stream); call(); } else if (leftVideo.mozCaptureStream) { stream = leftVideo.mozCaptureStream(); console.log('Captured stream from leftVideo with mozCaptureStream()', stream); call(); } else { console.log('captureStream() not supported'); } } // Video tag capture must be set up after video tracks are enumerated. leftVideo.oncanplay = maybeCreateStream; if (leftVideo.readyState >= 3) { // HAVE_FUTURE_DATA // Video is already ready to play, call maybeCreateStream in case oncanplay // fired before we registered the event handler. maybeCreateStream(); } leftVideo.play(); rightVideo.onloadedmetadata = () => { console.log(`Remote video videoWidth: ${rightVideo.videoWidth}px, videoHeight: ${rightVideo.videoHeight}px`); }; rightVideo.onresize = () => { console.log(`Remote video size changed to ${rightVideo.videoWidth}x${rightVideo.videoHeight}`); // We'll use the first onresize callback as an indication that // video has started playing out. if (startTime) { const elapsedTime = window.performance.now() - startTime; console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms'); startTime = null; } }; function call() { console.log('Starting call'); startTime = window.performance.now(); const videoTracks = stream.getVideoTracks(); const audioTracks = stream.getAudioTracks(); if (videoTracks.length > 0) { console.log(`Using video device: ${videoTracks[0].label}`); } if (audioTracks.length > 0) { console.log(`Using audio device: ${audioTracks[0].label}`); } const servers = null; pc1 = new RTCPeerConnection(servers); console.log('Created local peer connection object pc1'); pc1.onicecandidate = e => onIceCandidate(pc1, e); pc2 = new RTCPeerConnection(servers); console.log('Created remote peer connection object pc2'); pc2.onicecandidate = e => onIceCandidate(pc2, e); pc1.oniceconnectionstatechange = e => onIceStateChange(pc1, e); pc2.oniceconnectionstatechange = e => onIceStateChange(pc2, e); pc2.ontrack = gotRemoteStream; stream.getTracks().forEach(track => pc1.addTrack(track, stream)); console.log('Added local stream to pc1'); console.log('pc1 createOffer start'); pc1.createOffer(onCreateOfferSuccess, onCreateSessionDescriptionError, offerOptions); } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); } function onCreateOfferSuccess(desc) { console.log(`Offer from pc1 ${desc.sdp}`); console.log('pc1 setLocalDescription start'); pc1.setLocalDescription(desc, () => onSetLocalSuccess(pc1), onSetSessionDescriptionError); console.log('pc2 setRemoteDescription start'); pc2.setRemoteDescription(desc, () => onSetRemoteSuccess(pc2), onSetSessionDescriptionError); console.log('pc2 createAnswer start'); // Since the 'remote' side has no media stream we need // to pass in the right constraints in order for it to // accept the incoming offer of audio and video. pc2.createAnswer(onCreateAnswerSuccess, onCreateSessionDescriptionError); } function onSetLocalSuccess(pc) { console.log(`${getName(pc)} setLocalDescription complete`); } function onSetRemoteSuccess(pc) { console.log(`${getName(pc)} setRemoteDescription complete`); } function onSetSessionDescriptionError(error) { console.log(`Failed to set session description: ${error.toString()}`); } function gotRemoteStream(event) { if (rightVideo.srcObject !== event.streams[0]) { rightVideo.srcObject = event.streams[0]; console.log('pc2 received remote stream', event); } } function onCreateAnswerSuccess(desc) { console.log(`Answer from pc2: ${desc.sdp}`); console.log('pc2 setLocalDescription start'); pc2.setLocalDescription(desc, () => onSetLocalSuccess(pc2), onSetSessionDescriptionError); console.log('pc1 setRemoteDescription start'); pc1.setRemoteDescription(desc, () => onSetRemoteSuccess(pc1), onSetSessionDescriptionError); } function onIceCandidate(pc, event) { getOtherPc(pc).addIceCandidate(event.candidate) .then( () => onAddIceCandidateSuccess(pc), err => onAddIceCandidateError(pc, err) ); console.log(`${getName(pc)} ICE candidate: ${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess(pc) { console.log(`${getName(pc)} addIceCandidate success`); } function onAddIceCandidateError(pc, error) { console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`); } function onIceStateChange(pc, event) { if (pc) { console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`); console.log('ICE state change event: ', event); } } function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } ================================================ FILE: src/content/capture/video-video/css/main.css ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ video { margin: 0 10px 0 0; width: calc(50% - 7px); } video:last-of-type { margin-right: 0; } @media screen and (max-width: 400px) { video { margin: 0 5px 20px 0; width: calc(50% - 5px); } } ================================================ FILE: src/content/capture/video-video/index.html ================================================ captureStream(): video to video

WebRTC samples captureStream(): video to video

Press play on the left video to start the demo.

A stream is captured from the video element on the left using its captureStream() method and set as the srcObject of the video element on the right.

The stream variable are in global scope, so you can inspect them from the browser console.

View source on GitHub
================================================ FILE: src/content/capture/video-video/js/main.js ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const leftVideo = document.getElementById('leftVideo'); const rightVideo = document.getElementById('rightVideo'); leftVideo.addEventListener('canplay', () => { let stream; const fps = 0; if (leftVideo.captureStream) { stream = leftVideo.captureStream(fps); } else if (leftVideo.mozCaptureStream) { stream = leftVideo.mozCaptureStream(fps); } else { console.error('Stream capture is not supported'); stream = null; } rightVideo.srcObject = stream; }); ================================================ FILE: src/content/capture/worker-process/css/main.css ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ canvas { background-color: #ccc; --width: calc(45%); width: var(--width); height: calc(var(--width) * 0.75); margin: 1em; vertical-align: top; } video { --width: calc(45%); width: var(--width); height: calc(var(--width) * 0.75); margin: 1em; object-fit: cover; } ================================================ FILE: src/content/capture/worker-process/index.html ================================================ Video processing in a Worker

WebRTC samples Processing video data with a Worker

The camera is captured to a MediaStreamTrack, which is turned into a WHATWG Stream of ImageData objects by means of a canvas, and a red square is added.

The stream is sent to a Worker, which returns a new stream containing the same video data.

This is then mapped back to a MediaStream using another canvas.

The chief purpose of the demo is to demonstrate that this is doable, but that performance can be improved significantly.

NOTE: This works only on Chrome 76 and above with experimental Web features enabled, since it depends on transferable Streams.

A similar demo, without the worker process, is on the canvas filter demo.

View source on GitHub
================================================ FILE: src/content/capture/worker-process/js/main.js ================================================ /* * Copyright (c) 2019 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const source = document.querySelector('#source'); // TODO(hta): Use OffscreenCanvas for the intermediate canvases. const canvasIn = document.querySelector('#canvas-source'); const canvasOut = document.querySelector('#canvas-result'); const result = document.querySelector('#result'); const stream = canvasOut.captureStream(); let inputStream = null; let imageData = null; let transformStream = null; let writer = null; let reader = null; result.srcObject = stream; function loop() { if (source.videoWidth > 0 && source.videoHeight > 0) { canvasIn.width = source.videoWidth; canvasIn.height = source.videoHeight; const ctx = canvasIn.getContext('2d'); ctx.drawImage(source, 0, 0); // Put a red square into the image, to mark it as "processed". ctx.fillStyle = '#FF0000'; ctx.fillRect(10, 10, 80, 80); imageData = ctx.getImageData(0, 0, canvasIn.width, canvasIn.height); // At this point, we have data that can be transferred. writer.write(imageData); } window.requestAnimationFrame(loop); } // The read function paints the incoming data on the second canvas. const readData = async () => { const result = await reader.read(); if (!result.done) { canvasOut.width = source.videoWidth; canvasOut.height = source.videoHeight; const outCtx = canvasOut.getContext('2d'); outCtx.putImageData(result.value, 0, 0); readData(); } }; (async () => { inputStream = await navigator.mediaDevices.getUserMedia({video: true}); source.srcObject = inputStream; transformStream = new TransformStream(); writer = transformStream.writable.getWriter(); const myWorker = new Worker('js/worker.js'); myWorker.onmessage = function(e) { reader = e.data[1].getReader(); // Start the flow of data. readData(); window.requestAnimationFrame(loop); }; myWorker.postMessage(['stream', transformStream.readable], [transformStream.readable]); source.play(); result.play(); })(); ================================================ FILE: src/content/capture/worker-process/js/worker.js ================================================ /* * Copyright (c) 2019 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; onmessage = function(e) { const command = e.data[0]; if (command == 'stream') { const inputStream = e.data[1]; const transformStream = new TransformStream(); inputStream.pipeTo(transformStream.writable); postMessage(['response', transformStream.readable], [transformStream.readable]); } }; ================================================ FILE: src/content/datachannel/basic/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 1em 1em 0; width: 90px; } div#buttons { margin: 0 0 1em 0; } div#send { margin: 0 20px 1em 0; } div#sendReceive { border-bottom: 1px solid #eee; margin: 0; padding: 0 0 10px 0; } div#sendReceive > div { display: inline-block; width: calc(50% - 20px); } form { margin: 0 0 1em 0; white-space: nowrap; } form span { font-weight: 300; margin: 0 1em 0 0; white-space: normal; } textarea { color: #444; font-size: 0.9em; font-weight: 300; height: 7.0em; padding: 5px; width: calc(100% - 10px); } ================================================ FILE: src/content/datachannel/basic/index.html ================================================ Transmit text

WebRTC samples Transmit text

Send

Receive

View the console to see logging.

The RTCPeerConnection objects pc1 and pc2 are in global scope, so you can inspect them in the console as well.

For more information about RTCDataChannel, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/datachannel/basic/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; let pc1; let pc2; let sendChannel; let receiveChannel; const dataChannelSend = document.querySelector('textarea#dataChannelSend'); const dataChannelReceive = document.querySelector('textarea#dataChannelReceive'); const startButton = document.querySelector('button#startButton'); const sendButton = document.querySelector('button#sendButton'); const closeButton = document.querySelector('button#closeButton'); startButton.onclick = createConnection; sendButton.onclick = sendData; closeButton.onclick = closeDataChannels; function enableStartButton() { startButton.disabled = false; } function disableSendButton() { sendButton.disabled = true; } function createConnection() { dataChannelSend.placeholder = ''; const servers = null; pc1 = new RTCPeerConnection(servers); console.log('Created local peer connection object pc1'); sendChannel = pc1.createDataChannel('sendDataChannel'); console.log('Created send data channel'); pc1.onicecandidate = e => { onIceCandidate(pc1, e); }; sendChannel.onopen = onSendChannelStateChange; sendChannel.onclose = onSendChannelStateChange; pc2 = new RTCPeerConnection(servers); console.log('Created remote peer connection object pc2'); pc2.onicecandidate = e => { onIceCandidate(pc2, e); }; pc2.ondatachannel = receiveChannelCallback; pc1.createOffer().then( gotDescription1, onCreateSessionDescriptionError ); startButton.disabled = true; closeButton.disabled = false; } function onCreateSessionDescriptionError(error) { console.log('Failed to create session description: ' + error.toString()); } function sendData() { const data = dataChannelSend.value; sendChannel.send(data); console.log('Sent Data: ' + data); } function closeDataChannels() { console.log('Closing data channels'); sendChannel.close(); console.log('Closed data channel with label: ' + sendChannel.label); receiveChannel.close(); console.log('Closed data channel with label: ' + receiveChannel.label); pc1.close(); pc2.close(); pc1 = null; pc2 = null; console.log('Closed peer connections'); startButton.disabled = false; sendButton.disabled = true; closeButton.disabled = true; dataChannelSend.value = ''; dataChannelReceive.value = ''; dataChannelSend.disabled = true; disableSendButton(); enableStartButton(); } function gotDescription1(desc) { pc1.setLocalDescription(desc); console.log(`Offer from pc1\n${desc.sdp}`); pc2.setRemoteDescription(desc); pc2.createAnswer().then( gotDescription2, onCreateSessionDescriptionError ); } function gotDescription2(desc) { pc2.setLocalDescription(desc); console.log(`Answer from pc2\n${desc.sdp}`); pc1.setRemoteDescription(desc); } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function onIceCandidate(pc, event) { getOtherPc(pc) .addIceCandidate(event.candidate) .then( onAddIceCandidateSuccess, onAddIceCandidateError ); console.log(`${getName(pc)} ICE candidate: ${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess() { console.log('AddIceCandidate success.'); } function onAddIceCandidateError(error) { console.log(`Failed to add Ice Candidate: ${error.toString()}`); } function receiveChannelCallback(event) { console.log('Receive Channel Callback'); receiveChannel = event.channel; receiveChannel.onmessage = onReceiveMessageCallback; receiveChannel.onopen = onReceiveChannelStateChange; receiveChannel.onclose = onReceiveChannelStateChange; } function onReceiveMessageCallback(event) { console.log('Received Message'); dataChannelReceive.value = event.data; } function onSendChannelStateChange() { const readyState = sendChannel.readyState; console.log('Send channel state is: ' + readyState); if (readyState === 'open') { dataChannelSend.disabled = false; dataChannelSend.focus(); sendButton.disabled = false; closeButton.disabled = false; } else { dataChannelSend.disabled = true; sendButton.disabled = true; closeButton.disabled = true; } } function onReceiveChannelStateChange() { const readyState = receiveChannel.readyState; console.log(`Receive channel state is: ${readyState}`); } ================================================ FILE: src/content/datachannel/basic/js/test.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/datachannel/basic/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('datachannel basic', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('transfers text', async () => { const text = 'Hello world'; await driver.findElement(webdriver.By.id('startButton')).click(); await Promise.all([ driver.wait(() => driver.executeScript(() => { return pc1 && pc1.connectionState === 'connected'; // eslint-disable-line no-undef })), await driver.wait(() => driver.executeScript(() => { return pc2 && pc2.connectionState === 'connected'; // eslint-disable-line no-undef })), ]); await driver.wait(() => driver.findElement(webdriver.By.id('sendButton')).isEnabled()); await driver.findElement(webdriver.By.id('dataChannelSend')) .sendKeys(text); await driver.findElement(webdriver.By.id('sendButton')).click(); await driver.wait(() => driver.executeScript(() => { return document.getElementById('dataChannelReceive').value.length > 0; })); const value = await driver.findElement(webdriver.By.id('dataChannelReceive')).getAttribute('value'); expect(value).toBe(text); }); }); ================================================ FILE: src/content/datachannel/channel/css/main.css ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 1em 1em 0; width: 90px; } div#buttons { margin: 0 0 1em 0; } div#send { margin: 0 20px 1em 0; } div#sendReceive { border-bottom: 1px solid #eee; margin: 0; padding: 0 0 10px 0; } div#sendReceive > div { display: inline-block; width: calc(50% - 20px); } form { margin: 0 0 1em 0; white-space: nowrap; } form span { font-weight: 300; margin: 0 1em 0 0; white-space: normal; } textarea { color: #444; font-size: 0.9em; font-weight: 300; height: 7.0em; padding: 5px; width: calc(100% - 10px); } ================================================ FILE: src/content/datachannel/channel/index.html ================================================ Transmit text (between two tabs)

WebRTC samples Transmit text

Send

Receive

This sample shows how to setup a datachannel connection between two peers in different tabs using RTCPeerConnection and Broadcast Channel

Open the sample in two tabs (of the same browser), then click start in the first tab and send messages back and forth.

For more information about RTCDataChannel, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/datachannel/channel/js/main.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const startButton = document.getElementById('startButton'); const closeButton = document.getElementById('closeButton'); const sendButton = document.getElementById('sendButton'); sendButton.onclick = sendData; const dataChannelSend = document.querySelector('textarea#dataChannelSend'); const dataChannelReceive = document.querySelector('textarea#dataChannelReceive'); let pc; let sendChannel; let receiveChannel; const signaling = new BroadcastChannel('webrtc'); signaling.onmessage = e => { switch (e.data.type) { case 'offer': handleOffer(e.data); break; case 'answer': handleAnswer(e.data); break; case 'candidate': handleCandidate(e.data); break; case 'ready': // A second tab joined. This tab will enable the start button unless in a call already. if (pc) { console.log('already in call, ignoring'); return; } startButton.disabled = false; break; case 'bye': if (pc) { hangup(); } break; default: console.log('unhandled', e); break; } }; signaling.postMessage({type: 'ready'}); startButton.onclick = async () => { startButton.disabled = true; closeButton.disabled = false; await createPeerConnection(); sendChannel = pc.createDataChannel('sendDataChannel'); sendChannel.onopen = onSendChannelStateChange; sendChannel.onmessage = onSendChannelMessageCallback; sendChannel.onclose = onSendChannelStateChange; const offer = await pc.createOffer(); signaling.postMessage({type: 'offer', sdp: offer.sdp}); await pc.setLocalDescription(offer); }; closeButton.onclick = async () => { hangup(); signaling.postMessage({type: 'bye'}); }; async function hangup() { if (pc) { pc.close(); pc = null; } sendChannel = null; receiveChannel = null; console.log('Closed peer connections'); startButton.disabled = false; sendButton.disabled = true; closeButton.disabled = true; dataChannelSend.value = ''; dataChannelReceive.value = ''; dataChannelSend.disabled = true; }; function createPeerConnection() { pc = new RTCPeerConnection(); pc.onicecandidate = e => { const message = { type: 'candidate', candidate: null, }; if (e.candidate) { message.candidate = e.candidate.candidate; message.sdpMid = e.candidate.sdpMid; message.sdpMLineIndex = e.candidate.sdpMLineIndex; } signaling.postMessage(message); }; } async function handleOffer(offer) { if (pc) { console.error('existing peerconnection'); return; } await createPeerConnection(); pc.ondatachannel = receiveChannelCallback; await pc.setRemoteDescription(offer); const answer = await pc.createAnswer(); signaling.postMessage({type: 'answer', sdp: answer.sdp}); await pc.setLocalDescription(answer); } async function handleAnswer(answer) { if (!pc) { console.error('no peerconnection'); return; } await pc.setRemoteDescription(answer); } async function handleCandidate(candidate) { if (!pc) { console.error('no peerconnection'); return; } if (!candidate.candidate) { await pc.addIceCandidate(null); } else { await pc.addIceCandidate(candidate); } } function sendData() { const data = dataChannelSend.value; if (sendChannel) { sendChannel.send(data); } else { receiveChannel.send(data); } console.log('Sent Data: ' + data); } function receiveChannelCallback(event) { console.log('Receive Channel Callback'); receiveChannel = event.channel; receiveChannel.onmessage = onReceiveChannelMessageCallback; receiveChannel.onopen = onReceiveChannelStateChange; receiveChannel.onclose = onReceiveChannelStateChange; } function onReceiveChannelMessageCallback(event) { console.log('Received Message'); dataChannelReceive.value = event.data; } function onSendChannelMessageCallback(event) { console.log('Received Message'); dataChannelReceive.value = event.data; } function onSendChannelStateChange() { const readyState = sendChannel.readyState; console.log('Send channel state is: ' + readyState); if (readyState === 'open') { dataChannelSend.disabled = false; dataChannelSend.focus(); sendButton.disabled = false; closeButton.disabled = false; } else { dataChannelSend.disabled = true; sendButton.disabled = true; closeButton.disabled = true; } } function onReceiveChannelStateChange() { const readyState = receiveChannel.readyState; console.log(`Receive channel state is: ${readyState}`); if (readyState === 'open') { dataChannelSend.disabled = false; sendButton.disabled = false; closeButton.disabled = false; } else { dataChannelSend.disabled = true; sendButton.disabled = true; closeButton.disabled = true; } } ================================================ FILE: src/content/datachannel/channel/js/test.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/datachannel/channel/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('datachannel and broadcast channels', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(async () => { await driver.get(url); }); it('establishes a connection and sends a message back and forth', async () => { const firstHello = 'First tab to second tab'; const secondHello = 'Second tab to first tab'; const firstTab = await driver.getWindowHandle(); // Create a second tab, switch to it. await driver.switchTo().newWindow('tab'); const secondTab = await driver.getWindowHandle(); await driver.get(url); await driver.switchTo().window(firstTab); await driver.findElement(webdriver.By.id('startButton')).click(); // Assert state in first tab. await driver.switchTo().window(firstTab); await driver.wait(() => driver.executeScript(() => { return pc && pc.connectionState === 'connected'; // eslint-disable-line no-undef })); // Assert state in second tab. await driver.switchTo().window(secondTab); await driver.wait(() => driver.executeScript(() => { return pc && pc.connectionState === 'connected'; // eslint-disable-line no-undef })); // Send a message from the first tab to the second tab. await driver.switchTo().window(firstTab); await driver.wait(() => driver.findElement(webdriver.By.id('sendButton')).isEnabled()); await driver.findElement(webdriver.By.id('dataChannelSend')) .sendKeys(firstHello); await driver.findElement(webdriver.By.id('sendButton')).click(); // Assert it was received. await driver.switchTo().window(secondTab); await driver.wait(() => driver.executeScript(() => { return document.getElementById('dataChannelReceive').value.length > 0; })); const fromFirst= await driver.findElement(webdriver.By.id('dataChannelReceive')).getAttribute('value'); expect(fromFirst).toBe(firstHello); // Send a message from the second tab to the first tab. await driver.switchTo().window(secondTab); await driver.wait(() => driver.findElement(webdriver.By.id('sendButton')).isEnabled()); await driver.findElement(webdriver.By.id('dataChannelSend')) .sendKeys(secondHello); await driver.findElement(webdriver.By.id('sendButton')).click(); // Assert it was received. await driver.switchTo().window(firstTab); await driver.wait(() => driver.executeScript(() => { return document.getElementById('dataChannelReceive').value.length > 0; })); const fromSecond = await driver.findElement(webdriver.By.id('dataChannelReceive')).getAttribute('value'); expect(fromSecond).toBe(secondHello); }); }); ================================================ FILE: src/content/datachannel/datatransfer/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ div.progress { margin: 0 0 1em 0; } div.progress div.label { display: inline-block; font-weight: 400; width: 8.2em; } div.input { margin: 0 0 1em 0; } progress { width: calc(100% - 8.5em); } ================================================ FILE: src/content/datachannel/datatransfer/index.html ================================================ Generate and transfer data

WebRTC samples Generate and transfer data

This page generates and sends the specified amount of data via WebRTC datachannels.

To accomplish this in an interoperable way, the data is split into chunks which are then transferred via the datachannel. The datachannel is reliable and ordered by default which is well-suited to filetransfers.

Send and receive progress is monitored using HTML5 progress elements.

Send progress:
Receive progress:

View the console to see logging.

The RTCPeerConnection objects pc1 and pc2 are in global scope, so you can inspect them in the console as well.

For more information about RTCDataChannel, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/datachannel/datatransfer/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const MAX_CHUNK_SIZE = 262144; let pc1; let pc2; let sendChannel; let receiveChannel; let chunkSize; let lowWaterMark; let highWaterMark; let dataString; let timeoutHandle = null; const megsToSend = document.querySelector('input#megsToSend'); const sendButton = document.querySelector('button#sendTheData'); const orderedCheckbox = document.querySelector('input#ordered'); const sendProgress = document.querySelector('progress#sendProgress'); const receiveProgress = document.querySelector('progress#receiveProgress'); const errorMessage = document.querySelector('div#errorMsg'); const transferStatus = document.querySelector('span#transferStatus'); let bytesToSend = 0; let totalTimeUsedInSend = 0; let numberOfSendCalls = 0; let maxTimeUsedInSend = 0; let sendStartTime = 0; let currentThroughput = 0; sendButton.addEventListener('click', createConnection); // Prevent data sent to be set to 0. megsToSend.addEventListener('change', function() { const number = this.value; if (Number.isNaN(number)) { errorMessage.innerHTML = `Invalid value for MB to send: ${number}`; } else if (number <= 0) { sendButton.disabled = true; errorMessage.innerHTML = '

Please enter a number greater than zero.

'; } else if (number > 64) { sendButton.disabled = true; errorMessage.innerHTML = '

Please enter a number lower or equal than 64.

'; } else { errorMessage.innerHTML = ''; sendButton.disabled = false; } }); async function createConnection() { sendButton.disabled = true; megsToSend.disabled = true; const servers = null; const number = Number.parseInt(megsToSend.value); bytesToSend = number * 1024 * 1024; pc1 = new RTCPeerConnection(servers); // Let's make a data channel! const dataChannelParams = {ordered: false}; if (orderedCheckbox.checked) { dataChannelParams.ordered = true; } sendChannel = pc1.createDataChannel('sendDataChannel', dataChannelParams); sendChannel.addEventListener('open', onSendChannelOpen); sendChannel.addEventListener('close', onSendChannelClosed); console.log('Created send data channel: ', sendChannel); console.log('Created local peer connection object pc1: ', pc1); pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e)); pc2 = new RTCPeerConnection(servers); pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e)); pc2.addEventListener('datachannel', receiveChannelCallback); try { const localOffer = await pc1.createOffer(); await handleLocalDescription(localOffer); } catch (e) { console.error('Failed to create session description: ', e); } transferStatus.innerHTML = 'Peer connection setup complete.'; } function sendData() { // Stop scheduled timer if any (part of the workaround introduced below) if (timeoutHandle !== null) { clearTimeout(timeoutHandle); timeoutHandle = null; } let bufferedAmount = sendChannel.bufferedAmount; while (sendProgress.value < sendProgress.max) { transferStatus.innerText = 'Sending data...'; const timeBefore = performance.now(); sendChannel.send(dataString); const timeUsed = performance.now() - timeBefore; if (timeUsed > maxTimeUsedInSend) { maxTimeUsedInSend = timeUsed; totalTimeUsedInSend += timeUsed; } numberOfSendCalls += 1; bufferedAmount += chunkSize; sendProgress.value += chunkSize; // Pause sending if we reach the high water mark if (bufferedAmount >= highWaterMark) { // This is a workaround due to the bug that all browsers are incorrectly calculating the // amount of buffered data. Therefore, the 'bufferedamountlow' event would not fire. if (sendChannel.bufferedAmount < lowWaterMark) { timeoutHandle = setTimeout(() => sendData(), 0); } console.log(`Paused sending, buffered amount: ${bufferedAmount} (announced: ${sendChannel.bufferedAmount})`); break; } } if (sendProgress.value === sendProgress.max) { transferStatus.innerHTML = 'Data transfer completed successfully!'; } } function startSendingData() { transferStatus.innerHTML = 'Start sending data.'; sendProgress.max = bytesToSend; receiveProgress.max = sendProgress.max; sendProgress.value = 0; receiveProgress.value = 0; sendStartTime = performance.now(); maxTimeUsedInSend = 0; totalTimeUsedInSend = 0; numberOfSendCalls = 0; sendData(); } function maybeReset() { if (pc1 === null && pc2 === null) { sendButton.disabled = false; megsToSend.disabled = false; } } async function handleLocalDescription(desc) { pc1.setLocalDescription(desc); console.log('Offer from pc1:\n', desc.sdp); pc2.setRemoteDescription(desc); try { const remoteAnswer = await pc2.createAnswer(); handleRemoteAnswer(remoteAnswer); } catch (e) { console.error('Error when creating remote answer: ', e); } } function handleRemoteAnswer(desc) { pc2.setLocalDescription(desc); console.log('Answer from pc2:\n', desc.sdp); pc1.setRemoteDescription(desc); } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } async function onIceCandidate(pc, event) { const candidate = event.candidate; if (candidate === null) { return; } // Ignore null candidates try { await getOtherPc(pc).addIceCandidate(candidate); console.log('AddIceCandidate successful: ', candidate); } catch (e) { console.error('Failed to add Ice Candidate: ', e); } } function receiveChannelCallback(event) { console.log('Receive Channel Callback'); receiveChannel = event.channel; receiveChannel.binaryType = 'arraybuffer'; receiveChannel.addEventListener('close', onReceiveChannelClosed); receiveChannel.addEventListener('message', onReceiveMessageCallback); } function onReceiveMessageCallback(event) { receiveProgress.value += event.data.length; currentThroughput = receiveProgress.value / (performance.now() - sendStartTime); console.log('Current Throughput is:', currentThroughput, 'bytes/sec'); // Workaround for a bug in Chrome which prevents the closing event from being raised by the // remote side. Also a workaround for Firefox which does not send all pending data when closing // the channel. if (receiveProgress.value === receiveProgress.max) { sendChannel.close(); receiveChannel.close(); } } function onSendChannelOpen() { console.log('Send channel is open'); chunkSize = Math.min(pc1.sctp.maxMessageSize, MAX_CHUNK_SIZE); console.log('Determined chunk size: ', chunkSize); dataString = new Array(chunkSize).fill('X').join(''); lowWaterMark = chunkSize; // A single chunk highWaterMark = Math.max(chunkSize * 8, 1048576); // 8 chunks or at least 1 MiB console.log('Send buffer low water threshold: ', lowWaterMark); console.log('Send buffer high water threshold: ', highWaterMark); sendChannel.bufferedAmountLowThreshold = lowWaterMark; sendChannel.addEventListener('bufferedamountlow', (e) => { console.log('BufferedAmountLow event:', e); sendData(); }); startSendingData(); } function onSendChannelClosed() { console.log('Send channel is closed'); pc1.close(); pc1 = null; console.log('Closed local peer connection'); maybeReset(); console.log('Average time spent in send() (ms): ' + totalTimeUsedInSend / numberOfSendCalls); console.log('Max time spent in send() (ms): ' + maxTimeUsedInSend); const spentTime = performance.now() - sendStartTime; console.log('Total time spent: ' + spentTime); console.log('MBytes/Sec: ' + (bytesToSend / 1000) / spentTime); } function onReceiveChannelClosed() { console.log('Receive channel is closed'); pc2.close(); pc2 = null; console.log('Closed remote peer connection'); maybeReset(); } ================================================ FILE: src/content/datachannel/datatransfer/js/test.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/datachannel/datatransfer/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('datachannel datatransfer', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('transfers data', async () => { const megsToSend = 4; await driver.findElement(webdriver.By.id('megsToSend')) .clear(); await driver.findElement(webdriver.By.id('megsToSend')) .sendKeys(megsToSend + '\n'); await driver.findElement(webdriver.By.id('sendTheData')).click(); await Promise.all([ driver.wait(() => driver.executeScript(() => { return pc1 && pc1.connectionState === 'connected'; // eslint-disable-line no-undef })), await driver.wait(() => driver.executeScript(() => { return pc2 && pc2.connectionState === 'connected'; // eslint-disable-line no-undef })), ]); // the remote connection gets closed when it is done. await driver.wait(() => driver.executeScript(() => { return pc2 === null; // eslint-disable-line no-undef })); const transferred = await driver.findElement(webdriver.By.id('receiveProgress')).getAttribute('value'); expect(transferred >>> 0).toBe(megsToSend * 1024 * 1024); }); }); ================================================ FILE: src/content/datachannel/filetransfer/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ div.progress, div#bitrate { margin: 0 0 1em 0; } div.progress div.label { display: inline-block; font-weight: 400; width: 8.2em; } form { margin: 0 0 1em 0; white-space: nowrap; } progress { width: calc(100% - 8.5em); } ================================================ FILE: src/content/datachannel/filetransfer/index.html ================================================ Transfer a file

WebRTC samples Transfer a file

This page shows how to transfer a file via WebRTC datachannels.

To accomplish this in an interoperable way, the file is split into chunks which are then transferred via the datachannel. The datachannel is reliable and ordered by default which is well-suited to filetransfers.

Send and receive progress is monitored using HTML5 progress elements.

At the receiver, the file is reassembled using the Blob API and made available for download.

Note: real-world applications require a file transfer protocol to send metadata about the file (such as the filename, type, size, last modification date, hash, ...).This information can be conveyed either via the signaling channel or in-band. The demo elides this by assuming knowledge of the file size at the receiver and closes both the datachannel and the peerconnection when the correct amount of bytes has been received.

Send progress:
Receive progress:

View the console to see logging.

The RTCPeerConnection objects pc1 and pc2 are in global scope, so you can inspect them in the console as well.

For more information about RTCDataChannel, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/datachannel/filetransfer/js/main.js ================================================ /* eslint no-unused-expressions: 0 */ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; let pc1; let pc2; let sendChannel; let receiveChannel; let fileReader; const bitrateDiv = document.querySelector('div#bitrate'); const fileInput = document.querySelector('input#fileInput'); const abortButton = document.querySelector('button#abortButton'); const downloadAnchor = document.querySelector('a#download'); const sendProgress = document.querySelector('progress#sendProgress'); const receiveProgress = document.querySelector('progress#receiveProgress'); const statusMessage = document.querySelector('span#status'); const sendFileButton = document.querySelector('button#sendFile'); let receiveBuffer = []; let receivedSize = 0; let bytesPrev = 0; let timestampPrev = 0; let timestampStart; let statsInterval = null; let bitrateMax = 0; sendFileButton.addEventListener('click', () => createConnection()); fileInput.addEventListener('change', handleFileInputChange, false); abortButton.addEventListener('click', () => { if (fileReader && fileReader.readyState === 1) { console.log('Abort read!'); fileReader.abort(); } }); async function handleFileInputChange() { const file = fileInput.files[0]; if (!file) { console.log('No file chosen'); } else { sendFileButton.disabled = false; } } async function createConnection() { abortButton.disabled = false; sendFileButton.disabled = true; pc1 = new RTCPeerConnection(); console.log('Created local peer connection object pc1'); sendChannel = pc1.createDataChannel('sendDataChannel'); sendChannel.binaryType = 'arraybuffer'; console.log('Created send data channel'); sendChannel.addEventListener('open', onSendChannelStateChange); sendChannel.addEventListener('close', onSendChannelStateChange); sendChannel.addEventListener('error', onError); pc1.addEventListener('icecandidate', async event => { console.log('Local ICE candidate: ', event.candidate); await pc2.addIceCandidate(event.candidate); }); pc2 = new RTCPeerConnection(); console.log('Created remote peer connection object pc2'); pc2.addEventListener('icecandidate', async event => { console.log('Remote ICE candidate: ', event.candidate); await pc1.addIceCandidate(event.candidate); }); pc2.addEventListener('datachannel', receiveChannelCallback); try { const offer = await pc1.createOffer(); await gotLocalDescription(offer); } catch (e) { console.log('Failed to create session description: ', e); } fileInput.disabled = true; } function sendData() { const file = fileInput.files[0]; console.log(`File is ${[file.name, file.size, file.type, file.lastModified].join(' ')}`); // Handle 0 size files. statusMessage.textContent = ''; downloadAnchor.textContent = ''; if (file.size === 0) { bitrateDiv.innerHTML = ''; statusMessage.textContent = 'File is empty, please select a non-empty file'; closeDataChannels(); return; } sendProgress.max = file.size; receiveProgress.max = file.size; const chunkSize = 16384; fileReader = new FileReader(); let offset = 0; fileReader.addEventListener('error', error => console.error('Error reading file:', error)); fileReader.addEventListener('abort', event => console.log('File reading aborted:', event)); fileReader.addEventListener('load', e => { console.log('FileRead.onload ', e); sendChannel.send(e.target.result); offset += e.target.result.byteLength; sendProgress.value = offset; if (offset < file.size) { readSlice(offset); } }); const readSlice = o => { console.log('readSlice ', o); const slice = file.slice(offset, o + chunkSize); fileReader.readAsArrayBuffer(slice); }; readSlice(0); } function closeDataChannels() { console.log('Closing data channels'); sendChannel.close(); console.log(`Closed data channel with label: ${sendChannel.label}`); sendChannel = null; if (receiveChannel) { receiveChannel.close(); console.log(`Closed data channel with label: ${receiveChannel.label}`); receiveChannel = null; } pc1.close(); pc2.close(); pc1 = null; pc2 = null; console.log('Closed peer connections'); // re-enable the file select fileInput.disabled = false; abortButton.disabled = true; sendFileButton.disabled = false; } async function gotLocalDescription(desc) { await pc1.setLocalDescription(desc); console.log(`Offer from pc1\n ${desc.sdp}`); await pc2.setRemoteDescription(desc); try { const answer = await pc2.createAnswer(); await gotRemoteDescription(answer); } catch (e) { console.log('Failed to create session description: ', e); } } async function gotRemoteDescription(desc) { await pc2.setLocalDescription(desc); console.log(`Answer from pc2\n ${desc.sdp}`); await pc1.setRemoteDescription(desc); } function receiveChannelCallback(event) { console.log('Receive Channel Callback'); receiveChannel = event.channel; receiveChannel.binaryType = 'arraybuffer'; receiveChannel.onmessage = onReceiveMessageCallback; receiveChannel.onopen = onReceiveChannelStateChange; receiveChannel.onclose = onReceiveChannelStateChange; receivedSize = 0; bitrateMax = 0; downloadAnchor.textContent = ''; downloadAnchor.removeAttribute('download'); if (downloadAnchor.href) { URL.revokeObjectURL(downloadAnchor.href); downloadAnchor.removeAttribute('href'); } } function onReceiveMessageCallback(event) { console.log(`Received Message ${event.data.byteLength}`); receiveBuffer.push(event.data); receivedSize += event.data.byteLength; receiveProgress.value = receivedSize; // we are assuming that our signaling protocol told // about the expected file size (and name, hash, etc). const file = fileInput.files[0]; if (receivedSize === file.size) { const received = new Blob(receiveBuffer); receiveBuffer = []; downloadAnchor.href = URL.createObjectURL(received); downloadAnchor.download = file.name; downloadAnchor.textContent = `Click to download '${file.name}' (${file.size} bytes)`; downloadAnchor.style.display = 'block'; const bitrate = Math.round(receivedSize * 8 / ((new Date()).getTime() - timestampStart)); bitrateDiv.innerHTML = `Average Bitrate: ${bitrate} kbits/sec (max: ${bitrateMax} kbits/sec)`; if (statsInterval) { clearInterval(statsInterval); statsInterval = null; } closeDataChannels(); } } function onSendChannelStateChange() { if (sendChannel) { const {readyState} = sendChannel; console.log(`Send channel state is: ${readyState}`); if (readyState === 'open') { sendData(); } } } function onError(error) { if (sendChannel) { console.error('Error in sendChannel:', error); return; } console.log('Error in sendChannel which is already closed:', error); } async function onReceiveChannelStateChange() { if (receiveChannel) { const readyState = receiveChannel.readyState; console.log(`Receive channel state is: ${readyState}`); if (readyState === 'open') { timestampStart = (new Date()).getTime(); timestampPrev = timestampStart; statsInterval = setInterval(displayStats, 500); await displayStats(); } } } // display bitrate statistics. async function displayStats() { if (pc2 && pc2.iceConnectionState === 'connected') { const stats = await pc2.getStats(); let activeCandidatePair; stats.forEach(report => { if (report.type === 'transport') { activeCandidatePair = stats.get(report.selectedCandidatePairId); } }); if (activeCandidatePair) { if (timestampPrev === activeCandidatePair.timestamp) { return; } // calculate current bitrate const bytesNow = activeCandidatePair.bytesReceived; const bitrate = Math.round((bytesNow - bytesPrev) * 8 / (activeCandidatePair.timestamp - timestampPrev)); bitrateDiv.innerHTML = `Current Bitrate: ${bitrate} kbits/sec`; timestampPrev = activeCandidatePair.timestamp; bytesPrev = bytesNow; if (bitrate > bitrateMax) { bitrateMax = bitrate; } } } } ================================================ FILE: src/content/datachannel/filetransfer/js/test.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/datachannel/filetransfer/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('datachannel filetransfer', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('transfers a file', async () => { await driver.findElement(webdriver.By.id('fileInput')) .sendKeys(process.cwd() + '/src/content/devices/multi/images/poster.jpg'); await driver.wait(() => driver.findElement(webdriver.By.id('sendFile')).isEnabled()); await driver.findElement(webdriver.By.id('sendFile')).click(); // the remote connection gets closed when it is done. await driver.wait(() => driver.executeScript(() => { return pc2 === null; // eslint-disable-line no-undef })); await driver.wait(() => driver.findElement(webdriver.By.id('download')).isEnabled()); }); }); ================================================ FILE: src/content/datachannel/messaging/index.html ================================================ Send messages with datachannel

WebRTC samples Send messages with datachannel

This page show how to send text messages via WebRTC datachannels.

Enter a message in one text box and press send and it will be transferred to the "remote" peer over a datachannel.

View the console to see logging.

For more information about RTCDataChannel, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/datachannel/messaging/main.css ================================================ /* * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ div.messageBox { width: 100%; } textarea.message { width: 100%; height: 5em; resize: none; display: block; box-sizing: border-box; margin: 1em; } label { font-weight: 400; } ================================================ FILE: src/content/datachannel/messaging/main.js ================================================ /* * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint no-unused-expressions: 0 */ 'use strict'; import {LitElement, html} from 'https://unpkg.com/@polymer/lit-element@0.6.2?module'; class MessagingSample extends LitElement { constructor() { super(); this.connected = false; this.localMessages = ''; this.remoteMessages = ''; } disconnect() { this._pc1.close(); this._pc2.close(); } async connect() { console.log('connect!'); try { const dataChannelParams = {ordered: true}; this._pc1 = new RTCPeerConnection(); this._pc1.addEventListener('icecandidate', async e => { console.log('local connection ICE candidate: ', e.candidate); await this._pc2.addIceCandidate(e.candidate); }); this._pc2 = new RTCPeerConnection(); this._pc2.addEventListener('icecandidate', async e => { console.log('remote connection ICE candidate: ', e.candidate); await this._pc1.addIceCandidate(e.candidate); }); window.localChannel = this._localChannel = this._pc1 .createDataChannel('messaging-channel', dataChannelParams); this._localChannel.binaryType = 'arraybuffer'; this._localChannel.addEventListener('open', () => { console.log('Local channel open!'); this.connected = true; }); this._localChannel.addEventListener('close', () => { console.log('Local channel closed!'); this.connected = false; }); this._localChannel.addEventListener('message', this._onLocalMessageReceived.bind(this)); this._pc2.addEventListener('datachannel', this._onRemoteDataChannel.bind(this)); const initLocalOffer = async () => { const localOffer = await this._pc1.createOffer(); console.log(`Got local offer ${JSON.stringify(localOffer)}`); const localDesc = this._pc1.setLocalDescription(localOffer); const remoteDesc = this._pc2.setRemoteDescription(localOffer); return Promise.all([localDesc, remoteDesc]); }; const initRemoteAnswer = async () => { const remoteAnswer = await this._pc2.createAnswer(); console.log(`Got remote answer ${JSON.stringify(remoteAnswer)}`); const localDesc = this._pc2.setLocalDescription(remoteAnswer); const remoteDesc = this._pc1.setRemoteDescription(remoteAnswer); return Promise.all([localDesc, remoteDesc]); }; await initLocalOffer(); await initRemoteAnswer(); } catch (e) { console.log(e); } } _onLocalMessageReceived(event) { console.log(`Remote message received by local: ${event.data}`); this.localMessages += event.data + '\n'; } _onRemoteDataChannel(event) { console.log(`onRemoteDataChannel: ${JSON.stringify(event)}`); window.remoteChannel = this._remoteChannel = event.channel; this._remoteChannel.binaryType = 'arraybuffer'; this._remoteChannel.addEventListener('message', this._onRemoteMessageReceived.bind(this)); this._remoteChannel.addEventListener('close', () => { console.log('Remote channel closed!'); this.connected = false; }); } _onRemoteMessageReceived(event) { console.log(`Local message received by remote: ${event.data}`); this.remoteMessages += event.data + '\n'; } static get properties() { return { connected: {type: Boolean}, localMessages: {type: String}, remoteMessages: {type: String} }; } render() { return html`
`; } _sendMessage(selector, channel) { const textarea = this.shadowRoot.querySelector(selector); const value = textarea.value; if (value === '') { console.log('Not sending empty message!'); return; } console.log('Sending remote message: ', value); channel.send(value); textarea.value = ''; } } customElements.define('messaging-sample', MessagingSample); ================================================ FILE: src/content/devices/input-output/index.html ================================================ Select audio and video sources

WebRTC samplesSelect sources & outputs

Get available audio, video sources and audio output devices from mediaDevices.enumerateDevices() then set the source for getUserMedia() using a deviceId constraint.

Note: without permission, the browser will restrict the available devices to at most one per type.

Note: If you hear a reverb sound your microphone is picking up the output of your speakers/headset, lower the volume and/or move the microphone further away from your speakers/headset.

View source on GitHub
================================================ FILE: src/content/devices/input-output/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const videoElement = document.querySelector('video'); const audioInputSelect = document.querySelector('select#audioSource'); const audioOutputSelect = document.querySelector('select#audioOutput'); const videoSelect = document.querySelector('select#videoSource'); const selectors = [audioInputSelect, audioOutputSelect, videoSelect]; let hasMic = false; let hasCamera = false; let openMic = undefined; let openCamera = undefined; let hasPermission = false; audioOutputSelect.disabled = !('sinkId' in HTMLMediaElement.prototype); function getDevices() { navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(handleError); } function gotDevices(deviceInfos) { console.log('gotDevices', deviceInfos); hasMic = false; hasCamera = false; hasPermission = false; // Handles being called several times to update labels. Preserve values. const values = selectors.map(select => select.value); selectors.forEach(select => { while (select.firstChild) { select.removeChild(select.firstChild); } }); for (let i = 0; i !== deviceInfos.length; ++i) { const deviceInfo = deviceInfos[i]; if (deviceInfo.deviceId == '') { continue; } // If we get at least one deviceId, that means user has granted user // media permissions. hasPermission = true; const option = document.createElement('option'); option.value = deviceInfo.deviceId; if (deviceInfo.kind === 'audioinput') { hasMic = true; option.text = deviceInfo.label || `microphone ${audioInputSelect.length + 1}`; audioInputSelect.appendChild(option); } else if (deviceInfo.kind === 'audiooutput') { option.text = deviceInfo.label || `speaker ${audioOutputSelect.length + 1}`; audioOutputSelect.appendChild(option); } else if (deviceInfo.kind === 'videoinput') { hasCamera = true; option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`; videoSelect.appendChild(option); } else { console.log('Some other kind of source/device: ', deviceInfo); } } selectors.forEach((select, selectorIndex) => { if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) { select.value = values[selectorIndex]; } }); start(); } // Attach audio output device to video element using device/sink ID. function attachSinkId(element, sinkId) { if (typeof element.sinkId !== 'undefined') { element.setSinkId(sinkId) .then(() => { console.log(`Success, audio output device attached: ${sinkId}`); }) .catch(error => { let errorMessage = error; if (error.name === 'SecurityError') { errorMessage = `You need to use HTTPS for selecting audio output device: ${error}`; } console.error(errorMessage); // Jump back to first output device in the list as it's the default. audioOutputSelect.selectedIndex = 0; }); } else { console.warn('Browser does not support output device selection.'); } } function changeAudioDestination() { const audioDestination = audioOutputSelect.value; attachSinkId(videoElement, audioDestination); } function gotStream(stream) { window.stream = stream; // make stream available to console videoElement.srcObject = stream; if (stream.getVideoTracks()[0]) { openCamera = stream.getVideoTracks()[0].getSettings().deviceId; } if (stream.getAudioTracks()[0]) { openMic = stream.getAudioTracks()[0].getSettings().deviceId; } // Refresh list in case labels have become available return getDevices(); } function handleError(error) { console.log('navigator.MediaDevices.getUserMedia error: ', error.message, error.name); } function start() { const audioSource = audioInputSelect.value || undefined; const videoSource = videoSelect.value || undefined; // Don't open the same devices again. if (hasPermission && openMic == audioSource && openCamera == videoSource) { return; } // Close existng streams. if (window.stream) { window.stream.getTracks().forEach(track => { track.stop(); }); openCamera = undefined; openMic = undefined; } const constraints = { audio: true, video: true }; if (hasMic) { constraints['audio'] = {deviceId: audioSource ? {exact: audioSource} : undefined}; } if (hasCamera) { constraints['video'] = {deviceId: videoSource ? {exact: videoSource} : undefined}; } console.log('start', constraints); if (!hasPermission || hasCamera || hasMic) { navigator.mediaDevices.getUserMedia(constraints).then(gotStream).catch(handleError); } } audioInputSelect.onchange = start; audioOutputSelect.onchange = changeAudioDestination; videoSelect.onchange = start; navigator.mediaDevices.ondevicechange = getDevices; getDevices(); ================================================ FILE: src/content/devices/input-output/js/test.js ================================================ /* * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/devices/input-output/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('input-output', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('shows at least one audio input device', async () => { await driver.wait(driver.executeScript(() => { return document.getElementById('audioSource').childElementCount > 0; })); }); it('shows at least one video input device', async () => { await driver.wait(driver.executeScript(() => { return document.getElementById('videoSource').childElementCount > 0; })); }); it('shows at least one audio output device device', async function() { if (process.env.BROWSER === 'firefox') { this.skip(); } await driver.wait(driver.executeScript(() => { return document.getElementById('audioOutput').childElementCount > 0; })); }); }); ================================================ FILE: src/content/devices/multi/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ audio { margin: 0 0 1.5em 0; width: 100%; } div#sources > div { float: left; margin: 0 1em 0 0; width: calc(50% - 0.5em); } div#sources > div:last-of-type { margin: 0; } select { margin: 0 0 0.5em 0; } video { background: black; height: 234px; } ================================================ FILE: src/content/devices/multi/index.html ================================================ getUserMedia: output device selection

WebRTC samples getUserMedia: output device selection

getUserMedia():

Local media files:

This demo must be run from localhost or over HTTPS Chrome 49 or later, Firefox is not supported yet.

View source on GitHub
================================================ FILE: src/content/devices/multi/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const gumAudio = document.querySelector('audio.gum'); gumAudio.addEventListener('play', () => { gumAudio.volume = 0.1; console.log('Audio lowered to reduce feedback from local gUM stream'); }); const gumVideo = document.querySelector('video.gum'); gumVideo.addEventListener('play', () => { gumVideo.volume = 0.1; console.log('Audio lowered to reduce feedback from local gUM stream'); }); function gotDevices(deviceInfos) { const masterOutputSelector = document.createElement('select'); for (let i = 0; i !== deviceInfos.length; ++i) { const deviceInfo = deviceInfos[i]; const option = document.createElement('option'); option.value = deviceInfo.deviceId; if (deviceInfo.kind === 'audiooutput') { console.info('Found audio output device: ', deviceInfo.label); option.text = deviceInfo.label || `speaker ${masterOutputSelector.length + 1}`; masterOutputSelector.appendChild(option); } else { console.log('Found non audio output device: ', deviceInfo.label); } } // Clone the master outputSelector and replace outputSelector placeholders. const allOutputSelectors = document.querySelectorAll('select'); for (let selector = 0; selector < allOutputSelectors.length; selector++) { const newOutputSelector = masterOutputSelector.cloneNode(true); newOutputSelector.addEventListener('change', changeAudioDestination); allOutputSelectors[selector].parentNode.replaceChild(newOutputSelector, allOutputSelectors[selector]); } } navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(handleError); // Attach audio output device to the provided media element using the deviceId. function attachSinkId(element, sinkId, outputSelector) { if (typeof element.sinkId !== 'undefined') { element.setSinkId(sinkId) .then(() => { console.log(`Success, audio output device attached: ${sinkId} to element with ${element.title} as source.`); }) .catch(error => { let errorMessage = error; if (error.name === 'SecurityError') { errorMessage = `You need to use HTTPS for selecting audio output device: ${error}`; } console.error(errorMessage); // Jump back to first output device in the list as it's the default. outputSelector.selectedIndex = 0; }); } else { console.warn('Browser does not support output device selection.'); } } function changeAudioDestination(event) { const deviceId = event.target.value; const outputSelector = event.target; // FIXME: Make the media element lookup dynamic. const element = event.composedPath()[2].childNodes[1]; attachSinkId(element, deviceId, outputSelector); } function gotStream(stream) { window.stream = stream; // make stream available to console gumAudio.srcObject = stream; gumVideo.srcObject = stream; } function start() { if (window.stream) { window.stream.getTracks().forEach(track => { track.stop(); }); } const constraints = { audio: true, video: true }; navigator.mediaDevices.getUserMedia(constraints).then(gotStream).catch(handleError); } start(); function handleError(error) { console.log('navigator.MediaDevices.getUserMedia error: ', error.message, error.name); } ================================================ FILE: src/content/extensions/multipleroutes/src/README.md ================================================ ## Chrome WebRTC Network Limiter Configures the WebRTC traffic routing options in Chrome's privacy settings. ★ What it does: This configures WebRTC to not use certain IP addresses or protocols: - private IP addresses not visible to the public internet (e.g. addresses like 192.168.1.2) - any public IP addresses associated with network interfaces that are not used for web traffic (e.g. an ISP-provided address, when browsing through a VPN) - Require WebRTC traffic to go through proxy servers as configured in Chrome. Since most of the proxy servers don't handle UDP, this effectively turns off UDP until UDP proxy support is available in Chrome and such proxies are widely deployed. When the extension is installed on Chrome versions prior to M48, WebRTC will only use the public IP address associated with the interface used for web traffic, typically the same addresses that are already provided to sites in browser HTTP requests. For Chrome version M48 and after, this extension provides one more configuration which allows WebRTC to use both the default public address and, for machines behind a NAT, the default private address which is associated with the public one. Said behavior will be the default after a fresh installation of the extension to Chrome M48. For upgrade scenarios, the previous selected configuration should not be changed. The extension may also disable non-proxied UDP, but this is not on by default and must be configured using the extension's Options page. ★ Notes: This extension may affect the performance of applications that use WebRTC for audio/video or real-time data communication. Because it limits the potential network paths and protocols, WebRTC may pick a path which results in significantly longer delay or lower quality (e.g. through a VPN) or use TCP only through proxy servers which is not ideal for real-time communication. We are attempting to determine how common this is. By installing this item, you agree to the Google Terms of Service and Privacy Policy at https://www.google.com/intl/en/policies/. ================================================ FILE: src/content/extensions/multipleroutes/src/_locales/en/messages.json ================================================ { "NETLI_DEFAULT_RADIO": { "message": "Give me the best media experience: This option allows Chrome to explore all network paths to find the best way to send and receive media, which may be different from normal web traffic." }, "NETLI_DEFAULT_PUBLIC_AND_PRIVATE_INTERFACES_RADIO": { "message": "Use my default public and private IP addresses: This option forces Chrome to use the same network path for media as for normal web traffic, except when a web proxy is present. For machines behind a NAT, Chrome will also use the default private address to enhance connectivity. To prevent degraded performance, Chrome will attempt to send media directly instead of using the proxy." }, "NETLI_DEFAULT_PUBLIC_INTERFACE_ONLY_RADIO": { "message": "Use only my default public IP address: This option is the same as Use my default public and private IP addresses except that Chrome will not use the private default address." }, "NETLI_DISABLE_NON_PROXIED_UDP_RADIO": { "message": "Use my proxy server (if present): This option forces Chrome to use the same network path for media as for normal web traffic, including use of a web proxy. Chrome will always attempt to send media through the proxy, which will typically hurt media performance and increase the load on the proxy; furthermore, this behavior may be incompatible with some applications." }, "NETLI_OPTION_NOT_SUPPORTED": { "message": "Grayed out options require newer verion of Chrome." }, "NETLI_APPDESC": { "message": "Configures how WebRTC's network traffic is routed by changing Chrome's privacy settings." }, "NETLI_APPNAME": { "message": "WebRTC Network Limiter" }, "NETLI_OPTIONS": { "message": "WebRTC Network Limiter Options" } } ================================================ FILE: src/content/extensions/multipleroutes/src/manifest.json ================================================ { "default_locale": "en", "description": "__MSG_NETLI_APPDESC__", "icons": { "16": "img/icon_16.png", "128": "img/icon_128.png" }, "manifest_version": 3, "minimum_chrome_version": "42.0.2311.135", "name": "__MSG_NETLI_APPNAME__", "options_ui": { "open_in_tab": false, "page": "options.html" }, "permissions": [ "privacy" ], "version": "0.2.1.4" } ================================================ FILE: src/content/extensions/multipleroutes/src/options.html ================================================

================================================ FILE: src/content/extensions/multipleroutes/src/options.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const pn = chrome.privacy.network; const pi = chrome.privacy.IPHandlingPolicy; const mapPolicyToRadioId = {}; mapPolicyToRadioId[pi.DEFAULT] = 0; mapPolicyToRadioId[pi.DEFAULT_PUBLIC_AND_PRIVATE_INTERFACES] = 1; mapPolicyToRadioId[pi.DEFAULT_PUBLIC_INTERFACE_ONLY] = 2; mapPolicyToRadioId[pi.DISABLE_NON_PROXIED_UDP] = 3; const mapRadioIdToPolicy = {}; mapRadioIdToPolicy[0] = pi.DEFAULT; mapRadioIdToPolicy[1] = pi.DEFAULT_PUBLIC_AND_PRIVATE_INTERFACES; mapRadioIdToPolicy[2] = pi.DEFAULT_PUBLIC_INTERFACE_ONLY; mapRadioIdToPolicy[3] = pi.DISABLE_NON_PROXIED_UDP; // Saves options. function saveOptions() { const radios = document.getElementsByName('ip_policy_selection'); let i; for (i = 0; i < radios.length; i++) { if (radios[i].checked) { break; } } pn.webRTCIPHandlingPolicy.set({ value: mapRadioIdToPolicy[i] }); } function restoreRadios(policy) { const radios = document.getElementsByName('ip_policy_selection'); radios[mapPolicyToRadioId[policy]].checked = true; } function restoreOption() { pn.webRTCIPHandlingPolicy.get({}, function(details) { restoreRadios(details.value); }); } document.addEventListener('DOMContentLoaded', restoreOption); document.getElementById('default'). addEventListener('click', saveOptions); document.getElementById('default_public_and_private_interfaces'). addEventListener('click', saveOptions); document.getElementById('default_public_interface_only'). addEventListener('click', saveOptions); document.getElementById('disable_non_proxied_udp'). addEventListener('click', saveOptions); document.title = chrome.i18n.getMessage('netli_options'); const i18nElements = document.querySelectorAll('*[i18n-content]'); for (let i = 0; i < i18nElements.length; i++) { const elem = i18nElements[i]; const msg = elem.getAttribute('i18n-content'); elem.innerHTML = chrome.i18n.getMessage(msg); } function browserSupportsIPHandlingPolicy() { return pn.webRTCIPHandlingPolicy !== undefined; } if (browserSupportsIPHandlingPolicy()) { // Hide the 'not supported' banner. document.getElementById('not_supported').innerHTML = ''; } else { // Disable all options. for (let i = 0; i < 4; i++) { const key = 'Mode' + i; const section = document.getElementById(key); section.style.color = 'gray'; section.querySelector('input').disabled = true; } } ================================================ FILE: src/content/extensions/svc/css/main.css ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 0 0; width: 83px; } button#hangupButton { margin: 0; } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); margin: 0 0 20px 0; vertical-align: top; } video#localVideo { margin: 0 20px 20px 0; } div.box { margin: 1em; } @media screen and (max-width: 400px) { button { width: 83px; margin: 0 11px 10px 0; } video { height: 90px; margin: 0 0 10px 0; width: calc(50% - 7px); } video#localVideo { margin: 0 10px 20px 0; } } div.graph-container { float: left; margin: 0.5em; width: calc(50% - 1em); } ================================================ FILE: src/content/extensions/svc/index.html ================================================ Scalable Video Coding (SVC) Extension

WebRTC samples Peer connection

This sample shows how to setup a connection between two peers using RTCPeerConnection and choose the preferred video codec to use and scalability mode when the Scalable Video Coding (SVC) Extension is available.

Codec preferences:
Scalability Mode:
Bitrate
Packets sent per second
View source on GitHub
================================================ FILE: src/content/extensions/svc/js/main.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* global TimelineDataSeries, TimelineGraphView */ 'use strict'; const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); const hangupButton = document.getElementById('hangupButton'); callButton.disabled = true; hangupButton.disabled = true; startButton.addEventListener('click', start); callButton.addEventListener('click', call); hangupButton.addEventListener('click', hangup); let startTime; const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); localVideo.addEventListener('loadedmetadata', function() { console.log(`Local video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.addEventListener('loadedmetadata', function() { console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.addEventListener('resize', () => { console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`); // We'll use the first onsize callback as an indication that video has started // playing out. if (startTime) { const elapsedTime = window.performance.now() - startTime; console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms'); startTime = null; } }); const codecPreferences = document.querySelector('#codecPreferences'); const scalabilityMode = document.querySelector('#scalabilityMode'); const supportsSetCodecPreferences = window.RTCRtpTransceiver && 'setCodecPreferences' in window.RTCRtpTransceiver.prototype; const scalabilityModes = [ 'L1T1', 'L1T2', 'L1T3', 'L2T1', 'L2T2', 'L2T3', 'L3T1', 'L3T2', 'L3T3', 'L2T1h', 'L2T2h', 'L2T3h', 'S2T1', 'S2T2', 'S2T3', 'S2T1h', 'S2T2h', 'S2T3h', 'S3T1', 'S3T2', 'S3T3', 'S3T1h', 'S3T2h', 'S3T3h', 'L2T2_KEY', 'L2T3_KEY', 'L3T2_KEY', 'L3T3_KEY' ]; let localStream; let pc1; let pc2; let bitrateGraph; let bitrateSeries; let headerrateSeries; let packetGraph; let packetSeries; let lastResult; const offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 1 }; function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } async function start() { console.log('Requesting local stream'); startButton.disabled = true; try { const stream = await navigator.mediaDevices.getUserMedia({audio: false, video: true}); console.log('Received local stream'); localVideo.srcObject = stream; localStream = stream; callButton.disabled = false; } catch (e) { alert(`getUserMedia() error: ${e.name}`); } if (supportsSetCodecPreferences) { const {codecs} = RTCRtpReceiver.getCapabilities('video'); codecs.forEach(codec => { if (['video/red', 'video/ulpfec', 'video/rtx', 'video/flexfec-03'].includes(codec.mimeType)) { return; } const option = document.createElement('option'); option.value = (codec.mimeType + ' ' + (codec.sdpFmtpLine || '')).trim(); option.innerText = option.value; codecPreferences.appendChild(option); }); codecPreferences.addEventListener('change', async (event) => { const [mimeType] = event.target.value.split(' '); while (scalabilityMode.firstChild) { scalabilityMode.firstChild.remove(); } const option = document.createElement('option'); option.value = ''; option.innerText = 'NONE'; scalabilityMode.appendChild(option); const capabilityPromises = []; for (const mode of scalabilityModes) { capabilityPromises.push(navigator.mediaCapabilities.encodingInfo({ type: 'webrtc', video: { contentType: mimeType, width: 640, height: 480, bitrate: 10000, framerate: 29.97, scalabilityMode: mode }})); } const capabilityResults = await Promise.all(capabilityPromises); for (let i = 0; i < scalabilityModes.length; ++i) { if (capabilityResults[i].supported) { const option = document.createElement('option'); option.value = scalabilityModes[i]; option.innerText = scalabilityModes[i]; scalabilityMode.appendChild(option); } } if (scalabilityMode.childElementCount > 1) { scalabilityMode.disabled = false; } else { scalabilityMode.disabled = true; } }); codecPreferences.disabled = false; } bitrateSeries = new TimelineDataSeries(); bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas'); bitrateGraph.updateEndDate(); headerrateSeries = new TimelineDataSeries(); headerrateSeries.setColor('green'); packetSeries = new TimelineDataSeries(); packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas'); packetGraph.updateEndDate(); } async function call() { callButton.disabled = true; hangupButton.disabled = false; console.log('Starting call'); startTime = window.performance.now(); const videoTracks = localStream.getVideoTracks(); const audioTracks = localStream.getAudioTracks(); if (videoTracks.length > 0) { console.log(`Using video device: ${videoTracks[0].label}`); } if (audioTracks.length > 0) { console.log(`Using audio device: ${audioTracks[0].label}`); } const configuration = {}; console.log('RTCPeerConnection configuration:', configuration); pc1 = new RTCPeerConnection(configuration); console.log('Created local peer connection object pc1'); pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e)); pc2 = new RTCPeerConnection(configuration); console.log('Created remote peer connection object pc2'); pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e)); pc2.addEventListener('track', gotRemoteStream); const mode = scalabilityMode.value; localStream.getTracks().forEach((track) =>{ if (track.kind == 'video' && mode) { pc1.addTransceiver(track, { streams: [localStream], sendEncodings: [ {scalabilityMode: mode} ] }); } else { pc1.addTrack(track, localStream); } }); console.log('Added local stream to pc1'); codecPreferences.disabled = true; scalabilityMode.disabled = true; try { console.log('pc1 createOffer start'); const offer = await pc1.createOffer(offerOptions); await onCreateOfferSuccess(offer); } catch (e) { onCreateSessionDescriptionError(e); } } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); } async function onCreateOfferSuccess(desc) { console.log(`Offer from pc1\n${desc.sdp}`); console.log('pc1 setLocalDescription start'); try { await pc1.setLocalDescription(desc); onSetLocalSuccess(pc1); } catch (e) { onSetSessionDescriptionError(); } console.log('pc2 setRemoteDescription start'); try { await pc2.setRemoteDescription(desc); onSetRemoteSuccess(pc2); } catch (e) { onSetSessionDescriptionError(); } console.log('pc2 createAnswer start'); // Since the 'remote' side has no media stream we need // to pass in the right constraints in order for it to // accept the incoming offer of audio and video. try { const answer = await pc2.createAnswer(); await onCreateAnswerSuccess(answer); } catch (e) { onCreateSessionDescriptionError(e); } } function onSetLocalSuccess(pc) { console.log(`${getName(pc)} setLocalDescription complete`); } function onSetRemoteSuccess(pc) { console.log(`${getName(pc)} setRemoteDescription complete`); } function onSetSessionDescriptionError(error) { console.log(`Failed to set session description: ${error.toString()}`); } function gotRemoteStream(e) { if (remoteVideo.srcObject !== e.streams[0]) { remoteVideo.srcObject = e.streams[0]; console.log('pc2 received remote stream'); } if (supportsSetCodecPreferences) { const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex]; if (preferredCodec.value !== '') { const [mimeType, sdpFmtpLine] = preferredCodec.value.split(' '); const {codecs} = RTCRtpReceiver.getCapabilities('video'); const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.sdpFmtpLine === sdpFmtpLine); const selectedCodec = codecs[selectedCodecIndex]; codecs.splice(selectedCodecIndex, 1); codecs.unshift(selectedCodec); e.transceiver.setCodecPreferences(codecs); console.log('Preferred video codec', selectedCodec); } } } async function onCreateAnswerSuccess(desc) { console.log(`Answer from pc2:\n${desc.sdp}`); console.log('pc2 setLocalDescription start'); try { await pc2.setLocalDescription(desc); onSetLocalSuccess(pc2); } catch (e) { onSetSessionDescriptionError(e); } console.log('pc1 setRemoteDescription start'); try { await pc1.setRemoteDescription(desc); onSetRemoteSuccess(pc1); // Display the video codec that is actually used. setTimeout(async () => { const stats = await pc1.getStats(); stats.forEach(stat => { if (!(stat.type === 'outbound-rtp' && stat.kind === 'video')) { return; } const codec = stats.get(stat.codecId); document.getElementById('actualCodec').innerText = 'Using ' + codec.mimeType + ' ' + (codec.sdpFmtpLine ? codec.sdpFmtpLine + ' ' : '') + ', payloadType=' + codec.payloadType + '.'; }); }, 1000); } catch (e) { onSetSessionDescriptionError(e); } } async function onIceCandidate(pc, event) { try { await (getOtherPc(pc).addIceCandidate(event.candidate)); onAddIceCandidateSuccess(pc); } catch (e) { onAddIceCandidateError(pc, e); } console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess(pc) { console.log(`${getName(pc)} addIceCandidate success`); } function onAddIceCandidateError(pc, error) { console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`); } function hangup() { console.log('Ending call'); pc1.close(); pc2.close(); pc1 = null; pc2 = null; hangupButton.disabled = true; callButton.disabled = false; codecPreferences.disabled = false; scalabilityMode.disabled = false; } // query getStats every second window.setInterval(() => { if (!pc1) { return; } const sender = pc1.getSenders()[0]; if (!sender) { return; } sender.getStats().then(res => { res.forEach(report => { let bytes; let headerBytes; let packets; if (report.type === 'outbound-rtp') { if (report.isRemote) { return; } const now = report.timestamp; bytes = report.bytesSent; headerBytes = report.headerBytesSent; packets = report.packetsSent; if (lastResult && lastResult.has(report.id)) { // calculate bitrate const bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent) / (now - lastResult.get(report.id).timestamp); const headerrate = 8 * (headerBytes - lastResult.get(report.id).headerBytesSent) / (now - lastResult.get(report.id).timestamp); // append to chart bitrateSeries.addPoint(now, bitrate); headerrateSeries.addPoint(now, headerrate); bitrateGraph.setDataSeries([bitrateSeries, headerrateSeries]); bitrateGraph.updateEndDate(); // calculate number of packets and append to chart packetSeries.addPoint(now, packets - lastResult.get(report.id).packetsSent); packetGraph.setDataSeries([packetSeries]); packetGraph.updateEndDate(); } } }); lastResult = res; }); }, 1000); ================================================ FILE: src/content/getusermedia/audio/index.html ================================================ gUM audio

WebRTC samples getUserMedia, audio only

Warning: if you're not using headphones, pressing play will cause feedback.

Render the audio stream from an audio-only getUserMedia() call with an audio element.

The MediaStream object stream passed to the getUserMedia() callback is in global scope, so you can inspect it from the console.

View source on GitHub
================================================ FILE: src/content/getusermedia/audio/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; // Put variables in global scope to make them available to the browser console. const audio = document.querySelector('audio'); const constraints = window.constraints = { audio: true, video: false }; function handleSuccess(stream) { const audioTracks = stream.getAudioTracks(); console.log('Got stream with constraints:', constraints); console.log('Using audio device: ' + audioTracks[0].label); stream.oninactive = function() { console.log('Stream ended'); }; window.stream = stream; // make variable available to browser console audio.srcObject = stream; } function handleError(error) { const errorMessage = 'navigator.MediaDevices.getUserMedia error: ' + error.message + ' ' + error.name; document.getElementById('errorMsg').innerText = errorMessage; console.log(errorMessage); } navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).catch(handleError); ================================================ FILE: src/content/getusermedia/canvas/index.html ================================================ getUserMedia to canvas

WebRTC samples getUserMedia ⇒ canvas

Draw a frame from the video onto the canvas element using the drawImage() method.

The variables canvas, video and stream are in global scope, so you can inspect them from the console.

View source on GitHub
================================================ FILE: src/content/getusermedia/canvas/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; // Put variables in global scope to make them available to the browser console. const video = document.querySelector('video'); const canvas = window.canvas = document.querySelector('canvas'); canvas.width = 480; canvas.height = 360; const button = document.querySelector('button'); button.onclick = function() { canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); }; const constraints = { audio: false, video: true }; function handleSuccess(stream) { window.stream = stream; // make stream available to browser console video.srcObject = stream; } function handleError(error) { console.log('navigator.MediaDevices.getUserMedia error: ', error.message, error.name); } navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).catch(handleError); ================================================ FILE: src/content/getusermedia/exposure/index.html ================================================ Control Exposure

WebRTC samples Control Exposure

Exposure Mode:
Exposure Time:
Exposure Compensation:
Brightness:
White Balance Mode:

Display the video stream from getUserMedia() in a video element and control exposureMode, exposureTime and exposureCompensation if camera supports it.

The MediaStreamTrack object track is in global scope, so you can inspect it from the console.

View source on GitHub
================================================ FILE: src/content/getusermedia/exposure/js/main.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; // Put variables in global scope to make them available to the browser console. const constraints = window.constraints = { audio: false, video: true }; function handleSuccess(stream) { const video = document.querySelector('video'); const videoTracks = stream.getVideoTracks(); console.log('Got stream with constraints:', constraints); console.log(`Using video device: ${videoTracks[0].label}`); video.srcObject = stream; // make track variable available to browser console. [window.track] = stream.getVideoTracks(); loadProperties(); document.querySelector(`button[id=refreshControls]`).disabled = false; } function loadProperties(refreshValuesOnly) { const track = window.track; const capabilities = track.getCapabilities(); const settings = track.getSettings(); console.log('Capabilities: ', capabilities); console.log('Settings: ', settings); for (const property of ['exposureMode', 'exposureTime', 'exposureCompensation', 'brightness', 'whiteBalanceMode']) { // Check whether camera supports exposure. if (!(property in settings)) { errorMsg(`Camera does not support ${property}.`); continue; } let element; if (Array.isArray(capabilities[property])) { // Map it to a select element. const select = document.querySelector(`select[name=${property}]`); element = select; if (capabilities[property] && !refreshValuesOnly) { for (const mode of capabilities[property]) { select.insertAdjacentHTML('afterbegin', ``); } } } else { // Map it to a slider element. const input = document.querySelector(`input[name=${property}]`); element = input; input.min = capabilities[property].min; input.max = capabilities[property].max; input.step = capabilities[property].step; } element.value = settings[property]; element.disabled = false; if (!refreshValuesOnly) { element.oninput = async event => { try { const constraints = {advanced: [{[property]: element.value}]}; await track.applyConstraints(constraints); console.log('Did successfully apply new constraints: ', constraints); console.log('New camera settings: ', track.getSettings()); } catch (err) { console.error('applyConstraints() failed: ', err); } }; } } } function handleError(error) { if (error.name === 'NotAllowedError') { errorMsg('Permissions have not been granted to use your camera, ' + 'you need to allow the page access to your devices in ' + 'order for the demo to work.'); } errorMsg(`getUserMedia error: ${error.name}`, error); } function errorMsg(msg, error) { const errorElement = document.querySelector('#errorMsg'); errorElement.innerHTML += `

${msg}

`; if (typeof error !== 'undefined') { console.error(error); } } async function init(e) { try { const stream = await navigator.mediaDevices.getUserMedia(constraints); handleSuccess(stream); e.target.disabled = true; } catch (e) { handleError(e); } } document.querySelector('#showVideo').addEventListener('click', e => init(e)); ================================================ FILE: src/content/getusermedia/filter/index.html ================================================ getUserMedia + CSS filters

WebRTC samples getUserMedia + CSS filters

Draw a frame from the getUserMedia video stream onto the canvas element, then apply CSS filters.

The variables canvas, video and stream are in global scope, so you can inspect them from the console.

View source on GitHub
================================================ FILE: src/content/getusermedia/filter/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const snapshotButton = document.querySelector('button#snapshot'); const filterSelect = document.querySelector('select#filter'); // Put variables in global scope to make them available to the browser console. const video = window.video = document.querySelector('video'); const canvas = window.canvas = document.querySelector('canvas'); canvas.width = 480; canvas.height = 360; snapshotButton.onclick = function() { canvas.className = filterSelect.value; canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); }; filterSelect.onchange = function() { video.className = filterSelect.value; }; const constraints = { audio: false, video: true }; function handleSuccess(stream) { window.stream = stream; // make stream available to browser console video.srcObject = stream; } function handleError(error) { console.log('navigator.MediaDevices.getUserMedia error: ', error.message, error.name); } navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).catch(handleError); ================================================ FILE: src/content/getusermedia/getdisplaymedia/index.html ================================================ getDisplayMedia

WebRTC samples getDisplayMedia

Display the screensharing stream from getDisplayMedia() in a video element.

View source on GitHub
================================================ FILE: src/content/getusermedia/getdisplaymedia/js/main.js ================================================ /* * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const preferredDisplaySurface = document.getElementById('displaySurface'); const startButton = document.getElementById('startButton'); if (adapter.browserDetails.browser === 'chrome' && adapter.browserDetails.version >= 107) { // See https://developer.chrome.com/docs/web-platform/screen-sharing-controls/ document.getElementById('options').style.display = 'block'; } else if (adapter.browserDetails.browser === 'firefox') { // Polyfill in Firefox. // See https://blog.mozilla.org/webrtc/getdisplaymedia-now-available-in-adapter-js/ adapter.browserShim.shimGetDisplayMedia(window, 'screen'); } function handleSuccess(stream) { startButton.disabled = true; preferredDisplaySurface.disabled = true; const video = document.querySelector('video'); video.srcObject = stream; // demonstrates how to detect that the user has stopped // sharing the screen via the browser UI. stream.getVideoTracks()[0].addEventListener('ended', () => { errorMsg('The user has ended sharing the screen'); startButton.disabled = false; preferredDisplaySurface.disabled = false; }); } function handleError(error) { errorMsg(`getDisplayMedia error: ${error.name}`, error); } function errorMsg(msg, error) { const errorElement = document.querySelector('#errorMsg'); errorElement.innerHTML += `

${msg}

`; if (typeof error !== 'undefined') { console.error(error); } } startButton.addEventListener('click', () => { const options = {audio: true, video: true}; const displaySurface = preferredDisplaySurface.options[preferredDisplaySurface.selectedIndex].value; if (displaySurface !== 'default') { options.video = {displaySurface}; } navigator.mediaDevices.getDisplayMedia(options) .then(handleSuccess, handleError); }); if ((navigator.mediaDevices && 'getDisplayMedia' in navigator.mediaDevices)) { startButton.disabled = false; } else { errorMsg('getDisplayMedia is not supported'); } ================================================ FILE: src/content/getusermedia/gum/index.html ================================================ getUserMedia

WebRTC samples getUserMedia

Warning: if you're not using headphones, pressing play will cause feedback.

Display the video stream from getUserMedia() in a video element.

The MediaStream object stream passed to the getUserMedia() callback is in global scope, so you can inspect it from the console.

View source on GitHub
================================================ FILE: src/content/getusermedia/gum/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; // Put variables in global scope to make them available to the browser console. const constraints = window.constraints = { audio: false, video: true }; function handleSuccess(stream) { const video = document.querySelector('video'); const videoTracks = stream.getVideoTracks(); console.log('Got stream with constraints:', constraints); console.log(`Using video device: ${videoTracks[0].label}`); window.stream = stream; // make variable available to browser console video.srcObject = stream; } function handleError(error) { if (error.name === 'OverconstrainedError') { errorMsg(`OverconstrainedError: The constraints could not be satisfied by the available devices. Constraints: ${JSON.stringify(constraints)}`); } else if (error.name === 'NotAllowedError') { errorMsg('NotAllowedError: Permissions have not been granted to use your camera and ' + 'microphone, you need to allow the page access to your devices in ' + 'order for the demo to work.'); } errorMsg(`getUserMedia error: ${error.name}`, error); } function errorMsg(msg, error) { const errorElement = document.querySelector('#errorMsg'); errorElement.innerHTML += `

${msg}

`; if (typeof error !== 'undefined') { console.error(error); } } async function init(e) { try { const stream = await navigator.mediaDevices.getUserMedia(constraints); handleSuccess(stream); e.target.disabled = true; } catch (e) { handleError(e); } } document.querySelector('#showVideo').addEventListener('click', e => init(e)); ================================================ FILE: src/content/getusermedia/gum/js/test.js ================================================ /* * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/getusermedia/gum/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('getUserMedia', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('opens a camera', async () => { await driver.findElement(webdriver.By.css('button')).click(); await driver.wait(() => driver.executeScript(() => document.querySelector('video').readyState === HTMLMediaElement.HAVE_ENOUGH_DATA) ); const width = await driver.findElement(webdriver.By.css('video')).getAttribute('videoWidth'); expect(width >>> 0).toBeGreaterThan(320); }); }); ================================================ FILE: src/content/getusermedia/pan-tilt-zoom/index.html ================================================ Control camera pan, tilt, and zoom

WebRTC samples Control Pan-Tilt-Zoom Camera

Pan:
Tilt:
Zoom:

Display the video stream from getUserMedia() in a video element and control pan, tilt, and zoom if camera supports Pan-Tilt-Zoom.

The MediaStreamTrack object track is in global scope, so you can inspect it from the console.

View source on GitHub
================================================ FILE: src/content/getusermedia/pan-tilt-zoom/js/main.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; // Put variables in global scope to make them available to the browser console. const constraints = window.constraints = { video: { pan: true, tilt: true, zoom: true } }; function handleSuccess(stream) { const video = document.querySelector('video'); const videoTracks = stream.getVideoTracks(); console.log('Got stream with constraints:', constraints); console.log(`Using video device: ${videoTracks[0].label}`); video.srcObject = stream; // make track variable available to browser console. const [track] = [window.track] = stream.getVideoTracks(); const capabilities = track.getCapabilities(); const settings = track.getSettings(); for (const ptz of ['pan', 'tilt', 'zoom']) { // Check whether camera supports pan/tilt/zoom. if (!(ptz in settings)) { errorMsg(`Camera does not support ${ptz}.`); continue; } // Map it to a slider element. const input = document.querySelector(`input[name=${ptz}]`); input.min = capabilities[ptz].min; input.max = capabilities[ptz].max; input.step = capabilities[ptz].step; input.value = settings[ptz]; input.disabled = false; input.oninput = async event => { try { const constraints = {advanced: [{[ptz]: input.value}]}; await track.applyConstraints(constraints); } catch (err) { console.error('applyConstraints() failed: ', err); } }; } } function handleError(error) { if (error.name === 'NotAllowedError') { errorMsg('Permissions have not been granted to use your camera, ' + 'you need to allow the page access to your devices in ' + 'order for the demo to work.'); } errorMsg(`getUserMedia error: ${error.name}`, error); } function errorMsg(msg, error) { const errorElement = document.querySelector('#errorMsg'); errorElement.innerHTML += `

${msg}

`; if (typeof error !== 'undefined') { console.error(error); } } async function init(e) { try { const stream = await navigator.mediaDevices.getUserMedia(constraints); handleSuccess(stream); e.target.disabled = true; } catch (e) { handleError(e); } } document.querySelector('#showVideo').addEventListener('click', e => init(e)); ================================================ FILE: src/content/getusermedia/record/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 3px 10px 0; padding-left: 2px; padding-right: 2px; width: 120px; } /* copied from main button style */ .file-upload { background-color: #d84a38; border: none; border-radius: 2px; color: white; font-family: 'Roboto', sans-serif; font-size: 0.8em; margin: 0 0 1em 0; padding: 0.5em 0.7em 0.6em 0.7em; } .file-upload:hover { background-color: #cf402f; } button:last-of-type { margin: 0; } p.borderBelow { margin: 0 0 20px 0; padding: 0 0 20px 0; } video { vertical-align: top; --width: 25vw; width: var(--width); height: calc(var(--width) * 0.5625); } video:last-of-type { margin: 0 0 20px 0; } video#gumVideo { margin: 0 20px 20px 0; } input[type="file"] { display: none; } ================================================ FILE: src/content/getusermedia/record/index.html ================================================ MediaStream Recording

WebRTC samples MediaRecorder

For more information see the MediaStream Recording API Editor's Draft.

Recording format:

Media Stream Constraints options

Echo cancellation:

View source on GitHub
================================================ FILE: src/content/getusermedia/record/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ // This code is adapted from // https://rawgit.com/Miguelao/demos/master/mediarecorder.html 'use strict'; /* globals MediaRecorder */ let mediaRecorder; let recordedBlobs; const codecPreferences = document.querySelector('#codecPreferences'); const errorMsgElement = document.querySelector('span#errorMsg'); const recordedVideo = document.querySelector('video#recorded'); const recordButton = document.querySelector('button#record'); recordButton.addEventListener('click', () => { if (recordButton.textContent === 'Start Recording') { startRecording(); uploadButton.disabled = true; } else { stopRecording(); recordButton.textContent = 'Start Recording'; playButton.disabled = false; downloadButton.disabled = false; codecPreferences.disabled = false; } }); function doPlay(blob) { recordedVideo.src = null; recordedVideo.srcObject = null; recordedVideo.src = window.URL.createObjectURL(blob); recordedVideo.controls = true; recordedVideo.play(); }; const playButton = document.querySelector('button#play'); playButton.addEventListener('click', () => { const mimeType = codecPreferences.options[codecPreferences.selectedIndex].value.split(';', 1)[0]; const superBuffer = new Blob(recordedBlobs, {type: mimeType}); doPlay(superBuffer); }); const uploadButton = document.querySelector('#upload'); uploadButton.addEventListener('change', e => { doPlay(e.target.files[0]); }); const downloadButton = document.querySelector('button#download'); downloadButton.addEventListener('click', () => { const mimeType = codecPreferences.options[codecPreferences.selectedIndex].value.split(';', 1)[0]; const blob = new Blob(recordedBlobs, {type: mimeType}); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = `test.${mimeTypeToFileExtension(mimeType)}`; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 100); }); function mimeTypeToFileExtension(mimeType) { switch (mimeType) { case 'video/mp4': return 'mp4'; case 'video/webm': return 'webm'; case 'video/x-matroska': return 'mkv'; default: throw new Error(`unsupported mimetype: ${mimeType}`); } } function handleDataAvailable(event) { console.log('handleDataAvailable', event); if (event.data && event.data.size > 0) { recordedBlobs.push(event.data); } } function getSupportedMimeTypes() { const possibleTypes = [ 'video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/webm;codecs=h264,opus', 'video/webm;codecs=av01,opus', 'video/x-matroska;codecs=hvc1.1.6.L186.B0,opus', 'video/mp4;codecs=vp9,mp4a.40.2', 'video/mp4;codecs=vp9,opus', 'video/mp4;codecs=avc1.64003E,mp4a.40.2', 'video/mp4;codecs=avc1.64003E,opus', 'video/mp4;codecs=avc3.64003E,mp4a.40.2', 'video/mp4;codecs=avc3.64003E,opus', 'video/mp4;codecs=hvc1.1.6.L186.B0,mp4a.40.2', 'video/mp4;codecs=hvc1.1.6.L186.B0,opus', 'video/mp4;codecs=hev1.1.6.L186.B0,mp4a.40.2', 'video/mp4;codecs=hev1.1.6.L186.B0,opus', 'video/mp4;codecs=av01.0.19M.08,mp4a.40.2', 'video/mp4;codecs=av01.0.19M.08,opus', 'video/mp4', ]; return possibleTypes.filter(mimeType => { return MediaRecorder.isTypeSupported(mimeType); }); } async function startRecording() { recordedBlobs = []; const mimeType = codecPreferences.options[codecPreferences.selectedIndex].value; const options = {mimeType}; if (mimeType.split(';', 1)[0] === 'video/mp4') { // Adjust sampling rate to 48khz. const track = window.stream.getAudioTracks()[0]; if (track) { const {sampleRate} = track.getSettings(); if (sampleRate != 48000) { track.stop(); window.stream.removeTrack(track); const newStream = await navigator.mediaDevices.getUserMedia({audio: {sampleRate: 48000}}); window.stream.addTrack(newStream.getTracks()[0]); } } } try { mediaRecorder = new MediaRecorder(window.stream, options); } catch (e) { console.error('Exception while creating MediaRecorder:', e); errorMsgElement.innerHTML = `Exception while creating MediaRecorder: ${JSON.stringify(e)}`; return; } console.log('Created MediaRecorder', mediaRecorder, 'with options', options); recordButton.textContent = 'Stop Recording'; playButton.disabled = true; downloadButton.disabled = true; codecPreferences.disabled = true; mediaRecorder.onstop = (event) => { console.log('Recorder stopped: ', event); console.log('Recorded Blobs: ', recordedBlobs); }; mediaRecorder.ondataavailable = handleDataAvailable; mediaRecorder.start(); console.log('MediaRecorder started', mediaRecorder); } function stopRecording() { mediaRecorder.stop(); } function handleSuccess(stream) { recordButton.disabled = false; console.log('Got stream:', stream); window.stream = stream; const gumVideo = document.querySelector('video#gum'); gumVideo.srcObject = stream; getSupportedMimeTypes().forEach(mimeType => { const option = document.createElement('option'); option.value = mimeType; option.innerText = option.value; codecPreferences.appendChild(option); }); codecPreferences.disabled = false; } async function init(constraints, isGetDisplayMedia) { try { const stream = isGetDisplayMedia ? await navigator.mediaDevices.getDisplayMedia(constraints) : await navigator.mediaDevices.getUserMedia(constraints); handleSuccess(stream); } catch (e) { console.error('Source open error:', e); errorMsgElement.innerHTML = `Source error: ${e.toString()}`; } } async function onStart(isGetDisplayMedia) { document.querySelector('button#start-gum').disabled = true; document.querySelector('button#start-gdm').disabled = true; const hasEchoCancellation = document.querySelector('#echoCancellation').checked; const constraints = { audio: { echoCancellation: hasEchoCancellation }, video: { width: 1280, height: 720 } }; console.log('Using media constraints:', constraints); await init(constraints, isGetDisplayMedia); } document.querySelector('button#start-gum').addEventListener('click', async () => { await onStart(false); }); document.querySelector('button#start-gdm').addEventListener('click', async () => { await onStart(true); }); ================================================ FILE: src/content/getusermedia/resolution/index.html ================================================ getUserMedia: select resolution

WebRTC samples getUserMedia: select resolution

This example uses constraints.

Click a button to call getUserMedia() with appropriate resolution.

Lock video size
Lock aspect ratio
Pause video

For more information, see Capturing Audio & Video in HTML5 on HTML5 Rocks.

View source on GitHub
================================================ FILE: src/content/getusermedia/resolution/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const dimensions = document.querySelector('#dimensions'); const video = document.querySelector('video'); let stream; const qvgaButton = document.querySelector('#qvga'); const p180Button = document.querySelector('#p180'); const vgaButton = document.querySelector('#vga'); const p360Button = document.querySelector('#p360'); const hdButton = document.querySelector('#hd'); const fullHdButton = document.querySelector('#full-hd'); const cinemaFourKButton = document.querySelector('#cinemaFourK'); const televisionFourKButton = document.querySelector('#televisionFourK'); const eightKButton = document.querySelector('#eightK'); const videoblock = document.querySelector('#videoblock'); const messagebox = document.querySelector('#errormessage'); const widthInput = document.querySelector('div#width input'); const widthOutput = document.querySelector('div#width span'); const aspectLock = document.querySelector('#aspectlock'); const sizeLock = document.querySelector('#sizelock'); const pauseVideo = document.querySelector('#pausevideo'); const videoSelect = document.querySelector('select#videoSource'); let currentWidth = 0; let currentHeight = 0; p180Button.onclick = () => { getMedia(p180Constraints); }; qvgaButton.onclick = () => { getMedia(qvgaConstraints); }; p360Button.onclick = () => { getMedia(p360Constraints); }; vgaButton.onclick = () => { getMedia(vgaConstraints); }; hdButton.onclick = () => { getMedia(hdConstraints); }; fullHdButton.onclick = () => { getMedia(fullHdConstraints); }; televisionFourKButton.onclick = () => { getMedia(televisionFourKConstraints); }; cinemaFourKButton.onclick = () => { getMedia(cinemaFourKConstraints); }; eightKButton.onclick = () => { getMedia(eightKConstraints); }; pauseVideo.onchange = () => { if (pauseVideo.checked) { video.pause(); } else { video.play(); } }; const p180Constraints = { video: {width: {exact: 320}, height: {exact: 180}} }; const qvgaConstraints = { video: {width: {exact: 320}, height: {exact: 240}} }; const p360Constraints = { video: {width: {exact: 640}, height: {exact: 360}} }; const vgaConstraints = { video: {width: {exact: 640}, height: {exact: 480}} }; const hdConstraints = { video: {width: {exact: 1280}, height: {exact: 720}} }; const fullHdConstraints = { video: {width: {exact: 1920}, height: {exact: 1080}} }; const televisionFourKConstraints = { video: {width: {exact: 3840}, height: {exact: 2160}} }; const cinemaFourKConstraints = { video: {width: {exact: 4096}, height: {exact: 2160}} }; const eightKConstraints = { video: {width: {exact: 7680}, height: {exact: 4320}} }; function gotDevices(deviceInfos) { // Handles being called several times to update labels. Preserve values. while (videoSelect.firstChild) { videoSelect.removeChild(videoSelect.firstChild); } for (let i = 0; i !== deviceInfos.length; ++i) { const deviceInfo = deviceInfos[i]; const option = document.createElement('option'); option.value = deviceInfo.deviceId; if (deviceInfo.kind === 'videoinput') { option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`; videoSelect.appendChild(option); } } } function handleError(error) { console.log('navigator.MediaDevices.getUserMedia error: ', error.message, error.name); } navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(handleError); function gotStream(mediaStream) { stream = window.stream = mediaStream; // stream available to console video.srcObject = mediaStream; messagebox.style.display = 'none'; videoblock.style.display = 'block'; const track = mediaStream.getVideoTracks()[0]; const constraints = track.getConstraints(); console.log('Result constraints: ' + JSON.stringify(constraints)); if (constraints && constraints.width && constraints.width.exact) { widthInput.value = constraints.width.exact; widthOutput.textContent = constraints.width.exact; } else if (constraints && constraints.width && constraints.width.min) { widthInput.value = constraints.width.min; widthOutput.textContent = constraints.width.min; } } function errorMessage(who, what) { const message = who + ': ' + what; messagebox.innerText = message; messagebox.style.display = 'block'; console.log(message); } function clearErrorMessage() { messagebox.style.display = 'none'; } function displayVideoDimensions(whereSeen) { if (video.videoWidth) { dimensions.innerText = 'Actual video dimensions: ' + video.videoWidth + 'x' + video.videoHeight + 'px.'; if (currentWidth !== video.videoWidth || currentHeight !== video.videoHeight) { console.log(whereSeen + ': ' + dimensions.innerText); currentWidth = video.videoWidth; currentHeight = video.videoHeight; } } else { dimensions.innerText = 'Video not ready'; } } video.onloadedmetadata = () => { displayVideoDimensions('loadedmetadata'); }; video.onresize = () => { displayVideoDimensions('resize'); }; function constraintChange(e) { widthOutput.textContent = e.target.value; const track = window.stream.getVideoTracks()[0]; let constraints; if (aspectLock.checked) { constraints = { width: {exact: e.target.value}, aspectRatio: { exact: video.videoWidth / video.videoHeight } }; } else { constraints = {width: {exact: e.target.value}}; } clearErrorMessage(); console.log('applying ' + JSON.stringify(constraints)); track.applyConstraints(constraints) .then(() => { console.log('applyConstraint success'); displayVideoDimensions('applyConstraints'); }) .catch(err => { errorMessage('applyConstraints', err.name); }); } widthInput.onchange = constraintChange; sizeLock.onchange = () => { if (sizeLock.checked) { console.log('Setting fixed size'); video.style.width = '100%'; } else { console.log('Setting auto size'); video.style.width = 'auto'; } }; function getMedia(constraints) { if (stream) { stream.getTracks().forEach(track => { track.stop(); }); } clearErrorMessage(); videoblock.style.display = 'none'; constraints.video.deviceId = {exact: videoSelect.value}; console.log('getUserMedia constraints: ' + JSON.stringify(constraints)); navigator.mediaDevices.getUserMedia(constraints) .then(gotStream) .catch(e => { errorMessage('getUserMedia', e.message, e.name); }); } ================================================ FILE: src/content/getusermedia/resolution/js/test.js ================================================ /* * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/getusermedia/resolution/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('getUserMedia resolutions', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); const buttonToResolution = { 'p180': 320, 'p360': 640, 'qvga': 320, 'vga': 640, 'hd': 1280, 'full-hd': 1920, 'televisionFourK': 3840, /* TODO: unsupported by fake device? Or is fake device limited to physical device resolution? 'cinemaFourK': 4096, 'eightK': 8192, */ }; Object.keys(buttonToResolution).forEach(buttonId => { const resolution = buttonToResolution[buttonId]; it(`opens a camera with width ${resolution}px`, async () => { await driver.findElement(webdriver.By.id(buttonId)).click(); await driver.wait(() => driver.executeScript(() => document.querySelector('video').readyState === HTMLMediaElement.HAVE_ENOUGH_DATA) ); const width = await driver.findElement(webdriver.By.css('video')).getAttribute('videoWidth'); expect(width >>> 0).toBe(resolution); }); }); }); ================================================ FILE: src/content/getusermedia/source/index.html ================================================ Page move

The page has moved to: this page

================================================ FILE: src/content/getusermedia/volume/css/main.css ================================================ div#meters > div { margin: 0 0 1em 0; } div#meters div.label { display: inline-block; font-weight: 400; margin: 0 0.5em 0 0; width: 3.5em; } div#meters div.value { display: inline-block; } meter { width: 50%; } meter#clip { color: #db4437; } meter#slow { color: #f4b400; } meter#instant { color: #0f9d58; } ================================================ FILE: src/content/getusermedia/volume/index.html ================================================ Audio stream volume

WebRTC samples Audio stream volume

Measure the volume of a local media stream using WebAudio.

Instant:
Slow:
Clip:

The 'instant' volume changes approximately every 50ms; the 'slow' volume approximates the average volume over about a second.

Note that you will not hear your own voice; use the local audio rendering demo for that.

The audioContext, stream and soundMeter variables are in global scope, so you can inspect them from the console.

View source on GitHub
================================================ FILE: src/content/getusermedia/volume/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* global AudioContext, SoundMeter */ 'use strict'; const startButton = document.getElementById('startButton'); const stopButton = document.getElementById('stopButton'); startButton.onclick = start; stopButton.onclick = stop; const instantMeter = document.querySelector('#instant meter'); const slowMeter = document.querySelector('#slow meter'); const clipMeter = document.querySelector('#clip meter'); const instantValueDisplay = document.querySelector('#instant .value'); const slowValueDisplay = document.querySelector('#slow .value'); const clipValueDisplay = document.querySelector('#clip .value'); // Put variables in global scope to make them available to the browser console. const constraints = window.constraints = { audio: true, video: false }; let meterRefresh = null; function handleSuccess(stream) { // Put variables in global scope to make them available to the // browser console. window.stream = stream; const soundMeter = window.soundMeter = new SoundMeter(window.audioContext); soundMeter.connectToSource(stream, function(e) { if (e) { alert(e); return; } meterRefresh = setInterval(() => { instantMeter.value = instantValueDisplay.innerText = soundMeter.instant.toFixed(2); slowMeter.value = slowValueDisplay.innerText = soundMeter.slow.toFixed(2); clipMeter.value = clipValueDisplay.innerText = soundMeter.clip; }, 200); }); } function handleError(error) { console.log('navigator.MediaDevices.getUserMedia error: ', error.message, error.name); } function start() { console.log('Requesting local stream'); startButton.disabled = true; stopButton.disabled = false; try { window.AudioContext = window.AudioContext || window.webkitAudioContext; window.audioContext = new AudioContext(); } catch (e) { alert('Web Audio API not supported.'); } navigator.mediaDevices .getUserMedia(constraints) .then(handleSuccess) .catch(handleError); } function stop() { console.log('Stopping local stream'); startButton.disabled = false; stopButton.disabled = true; window.stream.getTracks().forEach(track => track.stop()); window.soundMeter.stop(); window.audioContext.close(); clearInterval(meterRefresh); instantMeter.value = instantValueDisplay.innerText = ''; slowMeter.value = slowValueDisplay.innerText = ''; clipMeter.value = clipValueDisplay.innerText = ''; } ================================================ FILE: src/content/getusermedia/volume/js/soundmeter.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; // Meter class that generates a number correlated to audio volume. // The meter class itself displays nothing, but it makes the // instantaneous and time-decaying volumes available for inspection. // It also reports on the fraction of samples that were at or near // the top of the measurement range. function SoundMeter(context) { this.context = context; this.instant = 0.0; this.slow = 0.0; this.clip = 0.0; this.node = null; } SoundMeter.prototype.connectToSource = async function(stream, callback) { console.log('SoundMeter connecting'); try { await this.context.audioWorklet.addModule('js/volume-meter-processor.js'); this.mic = this.context.createMediaStreamSource(stream); this.node = new AudioWorkletNode(this.context, 'volume-meter-processor'); this.node.port.onmessage = (event) => { const {instant, clip} = event.data; this.instant = instant; this.clip = clip; this.slow = 0.95 * this.slow + 0.05 * this.instant; }; this.mic.connect(this.node); if (typeof callback !== 'undefined') { callback(null); } } catch (e) { console.error(e); if (typeof callback !== 'undefined') { callback(e); } } }; SoundMeter.prototype.stop = function() { console.log('SoundMeter stopping'); this.mic.disconnect(); this.node.disconnect(); }; ================================================ FILE: src/content/getusermedia/volume/js/volume-meter-processor.js ================================================ /* * Copyright (c) 2025 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; // This class is used to compute the volume of the input audio stream. class VolumeMeterProcessor extends AudioWorkletProcessor { constructor() { super(); this._lastUpdate = Date.now(); } process(inputs) { // This example only supports mono channel. const input = inputs[0][0]; if (!input) { return true; } let sum = 0.0; let clipcount = 0; for (let i = 0; i < input.length; ++i) { sum += input[i] * input[i]; if (Math.abs(input[i]) > 0.99) { clipcount += 1; } } const instant = Math.sqrt(sum / input.length); this.port.postMessage({ instant: instant, clip: clipcount / input.length, }); return true; } } registerProcessor('volume-meter-processor', VolumeMeterProcessor); ================================================ FILE: src/content/insertable-streams/audio-processing/index.html ================================================ Insertable Streams - Audio

WebRTC samples Audio processing with insertable streams

This sample shows how to perform processing on an audio stream using the experimental insertable streams API. It applies a low-pass filter in realtime to audio recorded from a microphone and plays it back.

Warning: if you're not using headphones, pressing Start will cause feedback.

View the console to see logging. The audio, processor, generator, transformer, stream and processedStream variables are in global scope, so you can inspect them from the console. You may also adjust the level of filtering by assigning to cutoff.

Note: This sample is using an experimental API that has not yet been standardized. As of 2021-09-29, this API is available in Chrome M94.

View source on GitHub
================================================ FILE: src/content/insertable-streams/audio-processing/js/main.js ================================================ /* * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /* global MediaStreamTrackProcessor, MediaStreamTrackGenerator, AudioData */ if (typeof MediaStreamTrackProcessor === 'undefined' || typeof MediaStreamTrackGenerator === 'undefined') { alert( 'Your browser does not support the experimental MediaStreamTrack API ' + 'for Insertable Streams of Media. See the note at the bottom of the ' + 'page.'); } try { new MediaStreamTrackGenerator('audio'); console.log('Audio insertable streams supported'); } catch (e) { alert( 'Your browser does not support insertable audio streams. See the note ' + 'at the bottom of the page.'); } if (typeof AudioData === 'undefined') { alert( 'Your browser does not support WebCodecs. See the note at the bottom ' + 'of the page.'); } // Put variables in global scope to make them available to the browser console. // Audio element let audio; // Buttons let startButton; let stopButton; // Transformation chain elements let processor; let generator; // Stream from getUserMedia let stream; // Output from the transform let processedStream; // Worker for processing let worker; // Initialize on page load. async function init() { audio = document.getElementById('audioOutput'); startButton = document.getElementById('startButton'); stopButton = document.getElementById('stopButton'); startButton.onclick = start; stopButton.onclick = stop; } const constraints = window.constraints = { audio: true, video: false }; async function start() { startButton.disabled = true; try { stream = await navigator.mediaDevices.getUserMedia(constraints); const audioTracks = stream.getAudioTracks(); console.log('Using audio device: ' + audioTracks[0].label); stream.oninactive = () => { console.log('Stream ended'); }; processor = new MediaStreamTrackProcessor(audioTracks[0]); generator = new MediaStreamTrackGenerator('audio'); const source = processor.readable; const sink = generator.writable; worker = new Worker('js/worker.js'); worker.postMessage({source: source, sink: sink}, [source, sink]); processedStream = new MediaStream(); processedStream.addTrack(generator); audio.srcObject = processedStream; stopButton.disabled = false; await audio.play(); } catch (error) { const errorMessage = 'navigator.MediaDevices.getUserMedia error: ' + error.message + ' ' + error.name; document.getElementById('errorMsg').innerText = errorMessage; console.log(errorMessage); } } async function stop() { stopButton.disabled = true; audio.pause(); audio.srcObject = null; stream.getTracks().forEach(track => { track.stop(); }); worker.postMessage({command: 'abort'}); startButton.disabled = false; } window.onload = init; ================================================ FILE: src/content/insertable-streams/audio-processing/js/worker.js ================================================ /* * Copyright (c) 2023 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ // Adjust this value to increase/decrease the amount of filtering. // eslint-disable-next-line prefer-const let cutoff = 100; // Returns a low-pass transform function for use with TransformStream. function lowPassFilter() { const format = 'f32-planar'; let lastValuePerChannel = undefined; return (data, controller) => { const rc = 1.0 / (cutoff * 2 * Math.PI); const dt = 1.0 / data.sampleRate; const alpha = dt / (rc + dt); const nChannels = data.numberOfChannels; if (!lastValuePerChannel) { console.log(`Audio stream has ${nChannels} channels.`); lastValuePerChannel = Array(nChannels).fill(0); } const buffer = new Float32Array(data.numberOfFrames * nChannels); for (let c = 0; c < nChannels; c++) { const offset = data.numberOfFrames * c; const samples = buffer.subarray(offset, offset + data.numberOfFrames); data.copyTo(samples, {planeIndex: c, format}); let lastValue = lastValuePerChannel[c]; // Apply low-pass filter to samples. for (let i = 0; i < samples.length; ++i) { lastValue = lastValue + alpha * (samples[i] - lastValue); samples[i] = lastValue; } lastValuePerChannel[c] = lastValue; } controller.enqueue(new AudioData({ format, sampleRate: data.sampleRate, numberOfFrames: data.numberOfFrames, numberOfChannels: nChannels, timestamp: data.timestamp, data: buffer })); }; } let abortController; onmessage = async (event) => { if (event.data.command == 'abort') { abortController.abort(); abortController = null; } else { const source = event.data.source; const sink = event.data.sink; const transformer = new TransformStream({transform: lowPassFilter()}); abortController = new AbortController(); const signal = abortController.signal; const promise = source.pipeThrough(transformer, {signal}).pipeTo(sink); promise.catch((e) => { if (signal.aborted) { console.log('Shutting down streams after abort.'); } else { console.error('Error from stream transform:', e); } source.cancel(e); sink.abort(e); }); } }; ================================================ FILE: src/content/insertable-streams/endtoend-encryption/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 20px 10px 0 0; width: 100px; } div#buttons { margin: 0 0 20px 0; } div#status { height: 2em; margin: 1em 0 0 0; } input#audio { margin: 0 0.5em 0 0; position: relative; top: -1px; } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); } ================================================ FILE: src/content/insertable-streams/endtoend-encryption/index.html ================================================ Peer connection end to end encryption

WebRTC samples Peer connection end to end encryption

Sender and receiver

Sender and receiver

Crypto key: Encrypt first bytes:

Middlebox

Switch audio to middlebox:

View source on GitHub
================================================ FILE: src/content/insertable-streams/endtoend-encryption/js/main.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /* global RTCRtpScriptTransform */ /* global VideoPipe */ const video1 = document.querySelector('video#video1'); const video2 = document.querySelector('video#video2'); const videoMonitor = document.querySelector('#video-monitor'); const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); const hangupButton = document.getElementById('hangupButton'); const cryptoKey = document.querySelector('#crypto-key'); const cryptoOffsetBox = document.querySelector('#crypto-offset'); const banner = document.querySelector('#banner'); const muteMiddleBox = document.querySelector('#mute-middlebox'); startButton.onclick = start; callButton.onclick = call; hangupButton.onclick = hangup; cryptoKey.addEventListener('change', setCryptoKey); cryptoOffsetBox.addEventListener('change', setCryptoKey); muteMiddleBox.addEventListener('change', toggleMute); let startToMiddle; let startToEnd; let localStream; // eslint-disable-next-line no-unused-vars let remoteStream; // Preferring a certain codec is an expert option without GUI. // Use opus by default. // eslint-disable-next-line prefer-const let preferredAudioCodecMimeType = 'audio/opus'; // Use VP8 by default to limit depacketization issues. // eslint-disable-next-line prefer-const let preferredVideoCodecMimeType = 'video/VP8'; const supportsSetCodecPreferences = window.RTCRtpTransceiver && 'setCodecPreferences' in window.RTCRtpTransceiver.prototype; let hasEnoughAPIs = !!window.RTCRtpScriptTransform; if (!hasEnoughAPIs) { const supportsInsertableStreams = !!RTCRtpSender.prototype.createEncodedStreams; let supportsTransferableStreams = false; try { const stream = new ReadableStream(); window.postMessage(stream, '*', [stream]); supportsTransferableStreams = true; } catch (e) { console.error('Transferable streams are not supported.'); } hasEnoughAPIs = supportsInsertableStreams && supportsTransferableStreams; } if (!hasEnoughAPIs) { banner.innerText = 'Your browser does not support WebRTC Encoded Transforms. ' + 'This sample will not work.'; if (adapter.browserDetails.browser === 'chrome') { banner.innerText += ' Try with Enable experimental Web Platform features enabled from chrome://flags.'; } startButton.disabled = true; cryptoKey.disabled = true; cryptoOffsetBox.disabled = true; } function gotStream(stream) { console.log('Received local stream'); video1.srcObject = stream; localStream = stream; callButton.disabled = false; } function gotRemoteStream(stream) { console.log('Received remote stream'); remoteStream = stream; video2.srcObject = stream; } function start() { console.log('Requesting local stream'); startButton.disabled = true; const options = {audio: true, video: true}; navigator.mediaDevices .getUserMedia(options) .then(gotStream) .catch(function(e) { alert('getUserMedia() failed'); console.log('getUserMedia() error: ', e); }); } // We use a Worker to do the encryption and decryption. // See // https://developer.mozilla.org/en-US/docs/Web/API/Worker // for basic concepts. const worker = new Worker('./js/worker.js', {name: 'E2EE worker'}); function setupSenderTransform(sender) { if (window.RTCRtpScriptTransform) { sender.transform = new RTCRtpScriptTransform(worker, {operation: 'encode'}); return; } const senderStreams = sender.createEncodedStreams(); // Instead of creating the transform stream here, we do a postMessage to the worker. The first // argument is an object defined by us, the second is a list of variables that will be transferred to // the worker. See // https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage // If you want to do the operations on the main thread instead, comment out the code below. /* const transformStream = new TransformStream({ transform: encodeFunction, }); senderStreams.readable .pipeThrough(transformStream) .pipeTo(senderStreams.writable); */ const {readable, writable} = senderStreams; worker.postMessage({ operation: 'encode', readable, writable, }, [readable, writable]); } function setupReceiverTransform(receiver) { if (window.RTCRtpScriptTransform) { receiver.transform = new RTCRtpScriptTransform(worker, {operation: 'decode'}); return; } const receiverStreams = receiver.createEncodedStreams(); const {readable, writable} = receiverStreams; worker.postMessage({ operation: 'decode', readable, writable, }, [readable, writable]); } function maybeSetCodecPreferences(trackEvent) { if (!supportsSetCodecPreferences) return; if (trackEvent.track.kind === 'audio' && preferredAudioCodecMimeType ) { const {codecs} = RTCRtpReceiver.getCapabilities('audio'); const selectedCodecIndex = codecs.findIndex(c => c.mimeType === preferredAudioCodecMimeType); const selectedCodec = codecs[selectedCodecIndex]; codecs.splice(selectedCodecIndex, 1); codecs.unshift(selectedCodec); trackEvent.transceiver.setCodecPreferences(codecs); } else if (trackEvent.track.kind === 'video' && preferredVideoCodecMimeType) { const {codecs} = RTCRtpReceiver.getCapabilities('video'); const selectedCodecIndex = codecs.findIndex(c => c.mimeType === preferredVideoCodecMimeType); const selectedCodec = codecs[selectedCodecIndex]; codecs.splice(selectedCodecIndex, 1); codecs.unshift(selectedCodec); trackEvent.transceiver.setCodecPreferences(codecs); } } function call() { callButton.disabled = true; hangupButton.disabled = false; console.log('Starting call'); // The real use case is where the middle box relays the // packets and listens in, but since we don't have // access to raw packets, we just send the same video // to both places. startToMiddle = new VideoPipe(localStream, true, false, e => { // Do not setup the receiver transform. maybeSetCodecPreferences(e); videoMonitor.srcObject = e.streams[0]; }); startToMiddle.pc1.getSenders().forEach(setupSenderTransform); startToMiddle.negotiate(); startToEnd = new VideoPipe(localStream, true, true, e => { setupReceiverTransform(e.receiver); maybeSetCodecPreferences(e); gotRemoteStream(e.streams[0]); }); startToEnd.pc1.getSenders().forEach(setupSenderTransform); startToEnd.negotiate(); console.log('Video pipes created'); } function hangup() { console.log('Ending call'); startToMiddle.close(); startToEnd.close(); hangupButton.disabled = true; callButton.disabled = false; } function setCryptoKey(event) { console.log('Setting crypto key to ' + cryptoKey.value); const currentCryptoKey = cryptoKey.value; const useCryptoOffset = !cryptoOffsetBox.checked; if (currentCryptoKey) { banner.innerText = 'Encryption is ON'; } else { banner.innerText = 'Encryption is OFF'; } worker.postMessage({ operation: 'setCryptoKey', currentCryptoKey, useCryptoOffset, }); } function toggleMute(event) { video2.muted = muteMiddleBox.checked; videoMonitor.muted = !muteMiddleBox.checked; } ================================================ FILE: src/content/insertable-streams/endtoend-encryption/js/test.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/insertable-streams/endtoend-encryption/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('insertable streams e2ee', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('establishes a connection and hangs up', async () => { await driver.findElement(webdriver.By.id('startButton')).click(); await driver.wait(() => driver.executeScript(() => { return localStream !== null; // eslint-disable-line no-undef })); await driver.wait(() => driver.findElement(webdriver.By.id('callButton')).isEnabled()); await driver.findElement(webdriver.By.id('callButton')).click(); await Promise.all([ await driver.wait(() => driver.executeScript(() => { return startToEnd && startToEnd.pc1 && startToEnd.pc1.connectionState === 'connected'; // eslint-disable-line no-undef })), await driver.wait(() => driver.executeScript(() => { return startToEnd && startToEnd.pc2 && startToEnd.pc2.connectionState === 'connected'; // eslint-disable-line no-undef })), ]); await driver.wait(() => driver.executeScript(() => { return document.getElementById('video2').readyState === HTMLMediaElement.HAVE_ENOUGH_DATA; })); await driver.findElement(webdriver.By.id('hangupButton')).click(); await driver.wait(() => driver.executeScript(() => { return startToEnd && startToEnd.pc1 && startToEnd.pc1.connectionState === 'closed'; // eslint-disable-line no-undef })); }); it('establisheѕ a encrypted connection with a key set prior to connecting', async () => { await driver.findElement(webdriver.By.id('startButton')).click(); await driver.wait(() => driver.executeScript(() => { return localStream !== null; // eslint-disable-line no-undef })); await driver.findElement(webdriver.By.id('crypto-key')) .sendKeys('secret\n'); await driver.wait(() => driver.executeScript(() => { return document.getElementById('banner').innerText === 'Encryption is ON'; })); await driver.wait(() => driver.findElement(webdriver.By.id('callButton')).isEnabled()); await driver.findElement(webdriver.By.id('callButton')).click(); await driver.wait(() => driver.executeScript(() => { return document.getElementById('video2').readyState === HTMLMediaElement.HAVE_ENOUGH_DATA; })); }); }); ================================================ FILE: src/content/insertable-streams/endtoend-encryption/js/videopipe.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ // // A "videopipe" abstraction on top of WebRTC. // // The usage of this abstraction: // var pipe = new VideoPipe(mediastream, handlerFunction); // handlerFunction = function(MediaStreamTrackEvent) { // do_something // } // pipe.close(); // // The VideoPipe will set up 2 PeerConnections, connect them to each // other, and call HandlerFunction when the stream's track is available // in the second PeerConnection. // 'use strict'; function VideoPipe(stream, forceSend, forceReceive, handler) { this.pc1 = new RTCPeerConnection(); this.pc2 = new RTCPeerConnection(); this.pc2.ontrack = handler; stream.getTracks().forEach((track) => this.pc1.addTrack(track, stream)); } VideoPipe.prototype.negotiate = async function() { this.pc1.onicecandidate = e => this.pc2.addIceCandidate(e.candidate); this.pc2.onicecandidate = e => this.pc1.addIceCandidate(e.candidate); await this.pc1.setLocalDescription(); await this.pc2.setRemoteDescription(this.pc1.localDescription); await this.pc2.setLocalDescription(); await this.pc1.setRemoteDescription(this.pc2.localDescription); }; VideoPipe.prototype.close = function() { this.pc1.close(); this.pc2.close(); }; ================================================ FILE: src/content/insertable-streams/endtoend-encryption/js/worker.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* * This is a worker doing the encode/decode transformations to add end-to-end * encryption to a WebRTC PeerConnection using the Insertable Streams API. */ 'use strict'; let currentCryptoKey; let useCryptoOffset = true; let currentKeyIdentifier = 0; // If using crypto offset (controlled by a checkbox): // Do not encrypt the first couple of bytes of the payload. This allows // a middle to determine video keyframes or the opus mode being used. // For VP8 this is the content described in // https://tools.ietf.org/html/rfc6386#section-9.1 // which is 10 bytes for key frames and 3 bytes for delta frames. // For opus (where encodedFrame.type is not set) this is the TOC byte from // https://tools.ietf.org/html/rfc6716#section-3.1 // TODO: make this work for other codecs. // // It makes the (encrypted) video and audio much more fun to watch and listen to // as the decoder does not immediately throw a fatal error. const frameTypeToCryptoOffset = { key: 10, delta: 3, undefined: 1, }; function dump(encodedFrame, direction, max = 16) { const data = new Uint8Array(encodedFrame.data); let bytes = ''; for (let j = 0; j < data.length && j < max; j++) { bytes += (data[j] < 16 ? '0' : '') + data[j].toString(16) + ' '; } const metadata = encodedFrame.getMetadata(); console.log(performance.now().toFixed(2), direction, bytes.trim(), 'len=' + encodedFrame.data.byteLength, 'type=' + (encodedFrame.type || 'audio'), 'ts=' + (metadata.rtpTimestamp || encodedFrame.timestamp), 'ssrc=' + metadata.synchronizationSource, 'pt=' + (metadata.payloadType || '(unknown)'), 'mimeType=' + (metadata.mimeType || '(unknown)'), ); } let scount = 0; function encodeFunction(encodedFrame, controller) { if (scount++ < 30) { // dump the first 30 packets. dump(encodedFrame, 'send'); } if (currentCryptoKey) { const view = new DataView(encodedFrame.data); // Any length that is needed can be used for the new buffer. const newData = new ArrayBuffer(encodedFrame.data.byteLength + 5); const newView = new DataView(newData); const cryptoOffset = useCryptoOffset? frameTypeToCryptoOffset[encodedFrame.type] : 0; for (let i = 0; i < cryptoOffset && i < encodedFrame.data.byteLength; ++i) { newView.setInt8(i, view.getInt8(i)); } // This is a bitwise xor of the key with the payload. This is not strong encryption, just a demo. for (let i = cryptoOffset; i < encodedFrame.data.byteLength; ++i) { const keyByte = currentCryptoKey.charCodeAt(i % currentCryptoKey.length); newView.setInt8(i, view.getInt8(i) ^ keyByte); } // Append keyIdentifier. newView.setUint8(encodedFrame.data.byteLength, currentKeyIdentifier % 0xff); // Append checksum newView.setUint32(encodedFrame.data.byteLength + 1, 0xDEADBEEF); encodedFrame.data = newData; } controller.enqueue(encodedFrame); } let rcount = 0; function decodeFunction(encodedFrame, controller) { if (rcount++ < 30) { // dump the first 30 packets dump(encodedFrame, 'recv'); } const view = new DataView(encodedFrame.data); const checksum = encodedFrame.data.byteLength > 4 ? view.getUint32(encodedFrame.data.byteLength - 4) : false; if (currentCryptoKey) { if (checksum !== 0xDEADBEEF) { console.log('Corrupted frame received, checksum ' + checksum.toString(16)); return; // This can happen when the key is set and there is an unencrypted frame in-flight. } const keyIdentifier = view.getUint8(encodedFrame.data.byteLength - 5); if (keyIdentifier !== currentKeyIdentifier) { console.log(`Key identifier mismatch, got ${keyIdentifier} expected ${currentKeyIdentifier}.`); return; } const newData = new ArrayBuffer(encodedFrame.data.byteLength - 5); const newView = new DataView(newData); const cryptoOffset = useCryptoOffset? frameTypeToCryptoOffset[encodedFrame.type] : 0; for (let i = 0; i < cryptoOffset; ++i) { newView.setInt8(i, view.getInt8(i)); } for (let i = cryptoOffset; i < encodedFrame.data.byteLength - 5; ++i) { const keyByte = currentCryptoKey.charCodeAt(i % currentCryptoKey.length); newView.setInt8(i, view.getInt8(i) ^ keyByte); } encodedFrame.data = newData; } else if (checksum === 0xDEADBEEF) { return; // encrypted in-flight frame but we already forgot about the key. } controller.enqueue(encodedFrame); } function handleTransform(operation, readable, writable) { if (operation === 'encode') { const transformStream = new TransformStream({ transform: encodeFunction, }); readable .pipeThrough(transformStream) .pipeTo(writable); } else if (operation === 'decode') { const transformStream = new TransformStream({ transform: decodeFunction, }); readable .pipeThrough(transformStream) .pipeTo(writable); } } // Handler for messages, including transferable streams. onmessage = (event) => { if (event.data.operation === 'encode' || event.data.operation === 'decode') { return handleTransform(event.data.operation, event.data.readable, event.data.writable); } if (event.data.operation === 'setCryptoKey') { if (event.data.currentCryptoKey !== currentCryptoKey) { currentKeyIdentifier++; } currentCryptoKey = event.data.currentCryptoKey; useCryptoOffset = event.data.useCryptoOffset; } }; // Handler for RTCRtpScriptTransforms. if (self.RTCTransformEvent) { self.onrtctransform = (event) => { const transformer = event.transformer; handleTransform(transformer.options.operation, transformer.readable, transformer.writable); }; } ================================================ FILE: src/content/insertable-streams/video/index.html ================================================ Page move

The page has moved to: this page

================================================ FILE: src/content/insertable-streams/video-analyzer/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 0 0; width: 83px; } button#hangupButton { margin: 0; } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); margin: 0 0 20px 0; vertical-align: top; } video#localVideo { margin: 0 20px 20px 0; } div.box { margin: 1em; } @media screen and (max-width: 400px) { button { width: 83px; margin: 0 11px 10px 0; } video { height: 90px; margin: 0 0 10px 0; width: calc(50% - 7px); } video#localVideo { margin: 0 10px 20px 0; } } ================================================ FILE: src/content/insertable-streams/video-analyzer/index.html ================================================ Insertable Streams Video Analyzer

WebRTC samples Insertable Streams Video Analyzer

This sample shows how Insertable Streams can be used to analyze the encoded form of a video track.



View the console to see logging.

Video size:
Keyframe count:
Interframe count:
Last keyframe size:
Last interframe size:
Duplicate count:
View source on GitHub
================================================ FILE: src/content/insertable-streams/video-analyzer/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); const hangupButton = document.getElementById('hangupButton'); callButton.disabled = true; hangupButton.disabled = true; startButton.addEventListener('click', start); callButton.addEventListener('click', call); hangupButton.addEventListener('click', hangup); const smallButton = document.getElementById('size-small'); smallButton.addEventListener('click', () => { localStream.getVideoTracks()[0].applyConstraints({width: {exact: 180}}); }); const vgaButton = document.getElementById('size-vga'); vgaButton.addEventListener('click', () => { localStream.getVideoTracks()[0].applyConstraints({width: {exact: 640}}); }); const hdButton = document.getElementById('size-hd'); hdButton.addEventListener('click', () => { localStream.getVideoTracks()[0].applyConstraints({width: {exact: 1024}}); }); const banner = document.querySelector('#banner'); const supportsInsertableStreams = !!RTCRtpSender.prototype.createEncodedStreams; if (!supportsInsertableStreams) { banner.innerText = 'Your browser does not support Insertable Streams. ' + 'This sample will not work.'; startButton.disabled = true; } let startTime; const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); localVideo.addEventListener('loadedmetadata', function() { console.log(`Local video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.addEventListener('loadedmetadata', function() { if (startTime) { const elapsedTime = window.performance.now() - startTime; console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms'); startTime = null; } }); let localStream; let pc1; let pc2; const offerOptions = { offerToReceiveAudio: 0, offerToReceiveVideo: 1 }; function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } async function start() { console.log('Requesting local stream'); startButton.disabled = true; try { const stream = await navigator.mediaDevices.getUserMedia({video: true}); console.log('Received local stream'); localVideo.srcObject = stream; localStream = stream; callButton.disabled = false; smallButton.disabled = false; vgaButton.disabled = false; hdButton.disabled = false; } catch (e) { alert(`getUserMedia() error: ${e.name}`); } } async function call() { callButton.disabled = true; hangupButton.disabled = false; console.log('Starting call'); startTime = window.performance.now(); const videoTracks = localStream.getVideoTracks(); if (videoTracks.length > 0) { console.log(`Using video device: ${videoTracks[0].label}`); } pc1 = new RTCPeerConnection(); console.log('Created local peer connection object pc1'); pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e)); pc2 = new RTCPeerConnection(); console.log('Created remote peer connection object pc2'); pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e)); pc1.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1, e)); pc2.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2, e)); pc2.addEventListener('track', gotRemoteTrack); localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); console.log('Added local stream to pc1'); try { console.log('pc1 createOffer start'); const offer = await pc1.createOffer(offerOptions); await onCreateOfferSuccess(offer); } catch (e) { onCreateSessionDescriptionError(e); } } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); } async function onCreateOfferSuccess(desc) { console.log(`Offer from pc1\n${desc.sdp}`); console.log('pc1 setLocalDescription start'); try { await pc1.setLocalDescription(desc); onSetLocalSuccess(pc1); } catch (e) { onSetSessionDescriptionError(); } console.log('pc2 setRemoteDescription start'); try { await pc2.setRemoteDescription({type: 'offer', sdp: desc.sdp.replace('red/90000', 'green/90000')}); onSetRemoteSuccess(pc2); } catch (e) { onSetSessionDescriptionError(); } console.log('pc2 createAnswer start'); try { const answer = await pc2.createAnswer(); await onCreateAnswerSuccess(answer); } catch (e) { onCreateSessionDescriptionError(e); } } function onSetLocalSuccess(pc) { console.log(`${getName(pc)} setLocalDescription complete`); } function onSetRemoteSuccess(pc) { console.log(`${getName(pc)} setRemoteDescription complete`); } function onSetSessionDescriptionError(error) { console.log(`Failed to set session description: ${error.toString()}`); } function gotRemoteTrack(e) { console.log('pc2 received remote stream'); const frameStreams = e.receiver.createEncodedStreams(); frameStreams.readable.pipeThrough(new TransformStream({ transform: videoAnalyzer })) .pipeTo(frameStreams.writable); remoteVideo.srcObject = e.streams[0]; } async function onCreateAnswerSuccess(desc) { console.log(`Answer from pc2:\n${desc.sdp}`); console.log('pc2 setLocalDescription start'); try { await pc2.setLocalDescription(desc); onSetLocalSuccess(pc2); } catch (e) { onSetSessionDescriptionError(e); } console.log('pc1 setRemoteDescription start'); try { await pc1.setRemoteDescription(desc); onSetRemoteSuccess(pc1); } catch (e) { onSetSessionDescriptionError(e); } } async function onIceCandidate(pc, event) { try { await (getOtherPc(pc).addIceCandidate(event.candidate)); onAddIceCandidateSuccess(pc); } catch (e) { onAddIceCandidateError(pc, e); } console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess(pc) { console.log(`${getName(pc)} addIceCandidate success`); } function onAddIceCandidateError(pc, error) { console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`); } function onIceStateChange(pc, event) { if (pc) { console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`); console.log('ICE state change event: ', event); } } function hangup() { console.log('Ending call'); pc1.close(); pc2.close(); pc1 = null; pc2 = null; hangupButton.disabled = true; callButton.disabled = false; } const keyFrameCountDisplay = document.querySelector('#keyframe-count'); const keyFrameSizeDisplay = document.querySelector('#keyframe-size'); const interFrameCountDisplay = document.querySelector('#interframe-count'); const interFrameSizeDisplay = document.querySelector('#interframe-size'); const videoSizeDisplay = document.querySelector('#video-size'); const duplicateCountDisplay = document.querySelector('#duplicate-count'); let keyFrameCount = 0; let interFrameCount = 0; let keyFrameLastSize = 0; let interFrameLastSize = 0; let duplicateCount = 0; let prevFrameType; let prevFrameTimestamp; let prevFrameSynchronizationSource; function videoAnalyzer(encodedFrame, controller) { const view = new DataView(encodedFrame.data); // We assume that the video is VP8. // TODO: Check the codec to see that it is. // The lowest value bit in the first byte is the keyframe indicator. // https://tools.ietf.org/html/rfc6386#section-9.1 const keyframeBit = view.getUint8(0) & 0x01; // console.log(view.getUint8(0).toString(16)); if (keyframeBit === 0) { keyFrameCount++; keyFrameLastSize = encodedFrame.data.byteLength; } else { interFrameCount++; interFrameLastSize = encodedFrame.data.byteLength; } if (encodedFrame.type === prevFrameType && encodedFrame.timestamp === prevFrameTimestamp && encodedFrame.synchronizationSource === prevFrameSynchronizationSource) { duplicateCount++; } prevFrameType = encodedFrame.type; prevFrameTimestamp = encodedFrame.timestamp; prevFrameSynchronizationSource = encodedFrame.synchronizationSource; controller.enqueue(encodedFrame); } // Update the display of the counters once a second. setInterval(() => { keyFrameCountDisplay.innerText = keyFrameCount; keyFrameSizeDisplay.innerText = keyFrameLastSize; interFrameCountDisplay.innerText = interFrameCount; interFrameSizeDisplay.innerText = interFrameLastSize; duplicateCountDisplay.innerText = duplicateCount; }, 500); remoteVideo.addEventListener('resize', () => { console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`); // We'll use the first onsize callback as an indication that video has started // playing out. videoSizeDisplay.innerText = `${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`; }); ================================================ FILE: src/content/insertable-streams/video-crop/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 20px 10px 0 0; width: 100px; } div#buttons { margin: 0 0 20px 0; } div#status { height: 2em; margin: 1em 0 0 0; } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); } ================================================ FILE: src/content/insertable-streams/video-crop/index.html ================================================ Insertable Streams - Crop in a worker

WebRTC samples Breakout Box crop

This sample shows how to perform cropping on a video stream using the experimental mediacapture-transform API in a Worker.

Note: This sample is using an experimental API that has not yet been standardized. As of 2022-11-21, this API is available in the latest version of Chrome based browsers.

View source on GitHub
================================================ FILE: src/content/insertable-streams/video-crop/js/main.js ================================================ /* * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /* global MediaStreamTrackProcessor, MediaStreamTrackGenerator */ if (typeof MediaStreamTrackProcessor === 'undefined' || typeof MediaStreamTrackGenerator === 'undefined') { alert( 'Your browser does not support the experimental MediaStreamTrack API ' + 'for Insertable Streams of Media. See the note at the bottom of the ' + 'page.'); } const startButton = document.getElementById('startButton'); const localVideo = document.getElementById('localVideo'); const croppedVideo = document.getElementById('croppedVideo'); const worker = new Worker('./js/worker.js', {name: 'Crop worker'}); startButton.addEventListener('click', async () => { const stream = await navigator.mediaDevices.getUserMedia({video: {width: 1280, height: 720}}); localVideo.srcObject = stream; const [track] = stream.getTracks(); const processor = new MediaStreamTrackProcessor({track}); const {readable} = processor; const generator = new MediaStreamTrackGenerator({kind: 'video'}); const {writable} = generator; croppedVideo.srcObject = new MediaStream([generator]); worker.postMessage({ operation: 'crop', readable, writable, }, [readable, writable]); }); ================================================ FILE: src/content/insertable-streams/video-crop/js/worker.js ================================================ /* * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; function transform(frame, controller) { // Cropping from an existing video frame is supported by the API in Chrome 94+. const newFrame = new VideoFrame(frame, { visibleRect: { x: 320, width: 640, y: 180, height: 360, } }); controller.enqueue(newFrame); frame.close(); } onmessage = async (event) => { const {operation} = event.data; if (operation === 'crop') { const {readable, writable} = event.data; readable .pipeThrough(new TransformStream({transform})) .pipeTo(writable); } else { console.error('Unknown operation', operation); } }; ================================================ FILE: src/content/insertable-streams/video-processing/css/main.css ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ .video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); vertical-align: top; } .sourceVideo { margin: 0 20px 20px 0; } .sinkVideo { margin: 0 0 20px 0; } div.box { margin: 1em; } @media screen and (max-width: 400px) { .video { height: 90px; width: calc(50% - 7px); } .sourceVideo { margin: 0 10px 20px 0; } .sinkVideo { margin: 0 0 10px 0; } } ================================================ FILE: src/content/insertable-streams/video-processing/index.html ================================================ Insertable Streams - Video

WebRTC samples Video processing with insertable streams

This sample shows how to perform processing on a video stream using the experimental insertable streams API. There are options for the source of the input stream, the destination of the output stream, and the API used to transform the stream. There is also the option to duplicate the source stream to a video element on the page, which may affect the source FPS.

Source: Add to page:
Transform:
Destination:

View the console to see logging.

Note: This sample is using an experimental API that has not yet been standardized. This API is available in Chrome 94 or later.

View source on GitHub
================================================ FILE: src/content/insertable-streams/video-processing/js/camera-source.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /* global VideoMirrorHelper */ // defined in video-mirror-helper.js /** * Opens the device's camera with getUserMedia. * @implements {MediaStreamSource} in pipeline.js */ class CameraSource { // eslint-disable-line no-unused-vars constructor() { /** * @private @const {!VideoMirrorHelper} manages displaying the video stream * in the page */ this.videoMirrorHelper_ = new VideoMirrorHelper(); /** @private {?MediaStream} camera stream, initialized in getMediaStream */ this.stream_ = null; /** @private {string} */ this.debugPath_ = ''; } /** @override */ setDebugPath(path) { this.debugPath_ = path; this.videoMirrorHelper_.setDebugPath(`${path}.videoMirrorHelper_`); } /** @override */ setVisibility(visible) { this.videoMirrorHelper_.setVisibility(visible); } /** @override */ async getMediaStream() { if (this.stream_) return this.stream_; console.log('[CameraSource] Requesting camera.'); this.stream_ = await navigator.mediaDevices.getUserMedia({audio: false, video: true}); console.log( '[CameraSource] Received camera stream.', `${this.debugPath_}.stream_ =`, this.stream_); this.videoMirrorHelper_.setStream(this.stream_); return this.stream_; } /** @override */ destroy() { console.log('[CameraSource] Stopping camera'); this.videoMirrorHelper_.destroy(); if (this.stream_) { this.stream_.getTracks().forEach(t => t.stop()); } } } ================================================ FILE: src/content/insertable-streams/video-processing/js/canvas-source.js ================================================ /* * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const TEXT_SOURCE = 'https://raw.githubusercontent.com/w3c/mediacapture-insertable-streams/main/explainer.md'; const CANVAS_ASPECT_RATIO = 16 / 9; /** * @param {number} x * @return {number} x rounded to the nearest even integer */ function roundToEven(x) { return 2 * Math.round(x / 2); } /** * Draws text on a Canvas. * @implements {MediaStreamSource} in pipeline.js */ class CanvasSource { // eslint-disable-line no-unused-vars constructor() { /** @private {boolean} */ this.visibility_ = false; /** * @private {?HTMLCanvasElement} canvas element providing the MediaStream. */ this.canvas_ = null; /** * @private {?CanvasRenderingContext2D} the 2D context used to draw the * animation. */ this.ctx_ = null; /** * @private {?MediaStream} the MediaStream from captureStream. */ this.stream_ = null; /** * @private {?CanvasCaptureMediaStreamTrack} the capture track from * canvas_, obtained from stream_. We manually request new animation * frames on this track. */ this.captureTrack_ = null; /** @private {number} requestAnimationFrame handle */ this.requestAnimationFrameHandle_ = 0; /** @private {!Array} text to render */ this.text_ = ['WebRTC samples']; /** @private {string} */ this.debugPath_ = ''; fetch(TEXT_SOURCE) .then(response => { if (response.ok) { return response.text(); } throw new Error(`Request completed with status ${response.status}.`); }) .then(text => { this.text_ = text.trim().split('\n'); }) .catch((e) => { console.log(`[CanvasSource] The request to retrieve ${ TEXT_SOURCE} encountered an error: ${e}.`); }); } /** @override */ setDebugPath(path) { this.debugPath_ = path; } /** @override */ setVisibility(visible) { this.visibility_ = visible; if (this.canvas_) { this.updateCanvasVisibility(); } } /** @private */ updateCanvasVisibility() { if (this.canvas_.parentNode && !this.visibility_) { this.canvas_.parentNode.removeChild(this.canvas_); } else if (!this.canvas_.parentNode && this.visibility_) { console.log('[CanvasSource] Adding source canvas to page.'); const outputVideoContainer = document.getElementById('outputVideoContainer'); outputVideoContainer.parentNode.insertBefore( this.canvas_, outputVideoContainer); } } /** @private */ requestAnimationFrame() { this.requestAnimationFrameHandle_ = requestAnimationFrame(now => this.animate(now)); } /** * @private * @param {number} now current animation timestamp */ animate(now) { this.requestAnimationFrame(); const ctx = this.ctx_; if (!this.canvas_ || !ctx || !this.captureTrack_) { return; } // Resize canvas based on displayed size; or if not visible, based on the // output video size. // VideoFrame prefers to have dimensions that are even numbers. if (this.visibility_) { this.canvas_.width = roundToEven(this.canvas_.clientWidth); } else { const outputVideoContainer = document.getElementById('outputVideoContainer'); const outputVideo = outputVideoContainer.firstElementChild; if (outputVideo) { this.canvas_.width = roundToEven(outputVideo.clientWidth); } } this.canvas_.height = roundToEven(this.canvas_.width / CANVAS_ASPECT_RATIO); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, this.canvas_.width, this.canvas_.height); const linesShown = 20; const millisecondsPerLine = 1000; const linesIncludingExtraBlank = this.text_.length + linesShown; const totalAnimationLength = linesIncludingExtraBlank * millisecondsPerLine; const currentFrame = now % totalAnimationLength; const firstLineIdx = Math.floor( linesIncludingExtraBlank * (currentFrame / totalAnimationLength) - linesShown); const lineFraction = (now % millisecondsPerLine) / millisecondsPerLine; const border = 20; const fontSize = (this.canvas_.height - 2 * border) / (linesShown + 1); ctx.font = `${fontSize}px sansserif`; const textWidth = this.canvas_.width - 2 * border; // first line if (firstLineIdx >= 0) { const fade = Math.floor(256 * lineFraction); ctx.fillStyle = `rgb(${fade},${fade},${fade})`; const position = (2 - lineFraction) * fontSize; ctx.fillText(this.text_[firstLineIdx], border, position, textWidth); } // middle lines for (let line = 2; line <= linesShown - 1; line++) { const lineIdx = firstLineIdx + line - 1; if (lineIdx >= 0 && lineIdx < this.text_.length) { ctx.fillStyle = 'black'; const position = (line + 1 - lineFraction) * fontSize; ctx.fillText(this.text_[lineIdx], border, position, textWidth); } } // last line const lastLineIdx = firstLineIdx + linesShown - 1; if (lastLineIdx >= 0 && lastLineIdx < this.text_.length) { const fade = Math.floor(256 * (1 - lineFraction)); ctx.fillStyle = `rgb(${fade},${fade},${fade})`; const position = (linesShown + 1 - lineFraction) * fontSize; ctx.fillText(this.text_[lastLineIdx], border, position, textWidth); } this.captureTrack_.requestFrame(); } /** @override */ async getMediaStream() { if (this.stream_) return this.stream_; console.log('[CanvasSource] Initializing 2D context for source animation.'); this.canvas_ = /** @type {!HTMLCanvasElement} */ (document.createElement('canvas')); this.canvas_.classList.add('video', 'sourceVideo'); // Generally video frames do not have an alpha channel. Even if the browser // supports it, there may be a performance cost, so we disable alpha. this.ctx_ = /** @type {?CanvasRenderingContext2D} */ ( this.canvas_.getContext('2d', {alpha: false})); if (!this.ctx_) { throw new Error('Unable to create CanvasRenderingContext2D'); } this.updateCanvasVisibility(); this.stream_ = this.canvas_.captureStream(0); this.captureTrack_ = /** @type {!CanvasCaptureMediaStreamTrack} */ ( this.stream_.getTracks()[0]); this.requestAnimationFrame(); console.log( '[CanvasSource] Initialized canvas, context, and capture stream.', `${this.debugPath_}.canvas_ =`, this.canvas_, `${this.debugPath_}.ctx_ =`, this.ctx_, `${this.debugPath_}.stream_ =`, this.stream_, `${this.debugPath_}.captureTrack_ =`, this.captureTrack_); return this.stream_; } /** @override */ destroy() { console.log('[CanvasSource] Stopping source animation'); if (this.requestAnimationFrameHandle_) { cancelAnimationFrame(this.requestAnimationFrameHandle_); } if (this.canvas_) { if (this.canvas_.parentNode) { this.canvas_.parentNode.removeChild(this.canvas_); } } } } ================================================ FILE: src/content/insertable-streams/video-processing/js/canvas-transform.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /** * Applies a picture-frame effect using CanvasRenderingContext2D. * @implements {FrameTransform} in pipeline.js */ class CanvasTransform { // eslint-disable-line no-unused-vars constructor() { /** * @private {?OffscreenCanvas} canvas used to create the 2D context. * Initialized in init. */ this.canvas_ = null; /** * @private {?CanvasRenderingContext2D} the 2D context used to draw the * effect. Initialized in init. */ this.ctx_ = null; /** @private {string} */ this.debugPath_ = 'debug.pipeline.frameTransform_'; } /** @override */ async init() { console.log('[CanvasTransform] Initializing 2D context for transform'); this.canvas_ = new OffscreenCanvas(1, 1); this.ctx_ = /** @type {?CanvasRenderingContext2D} */ ( this.canvas_.getContext('2d', {alpha: false, desynchronized: true})); if (!this.ctx_) { throw new Error('Unable to create CanvasRenderingContext2D'); } console.log( '[CanvasTransform] CanvasRenderingContext2D initialized.', `${this.debugPath_}.canvas_ =`, this.canvas_, `${this.debugPath_}.ctx_ =`, this.ctx_); } /** @override */ async transform(frame, controller) { const ctx = this.ctx_; if (!this.canvas_ || !ctx) { frame.close(); return; } const width = frame.displayWidth; const height = frame.displayHeight; this.canvas_.width = width; this.canvas_.height = height; const timestamp = frame.timestamp; ctx.drawImage(frame, 0, 0); frame.close(); ctx.shadowColor = '#000'; ctx.shadowBlur = 20; ctx.lineWidth = 50; ctx.strokeStyle = '#000'; ctx.strokeRect(0, 0, width, height); // alpha: 'discard' is needed in order to send frames to a PeerConnection. controller.enqueue(new VideoFrame(this.canvas_, {timestamp, alpha: 'discard'})); } /** @override */ destroy() {} } ================================================ FILE: src/content/insertable-streams/video-processing/js/main.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /* global MediaStreamTrackProcessor, MediaStreamTrackGenerator */ if (typeof MediaStreamTrackProcessor === 'undefined' || typeof MediaStreamTrackGenerator === 'undefined') { alert( 'Your browser does not support the experimental MediaStreamTrack API ' + 'for Insertable Streams of Media. See the note at the bottom of the ' + 'page.'); } /* global CameraSource */ // defined in camera-source.js /* global CanvasSource */ // defined in canvas-source.js /* global CanvasTransform */ // defined in canvas-transform.js /* global PeerConnectionSink */ // defined in peer-connection-sink.js /* global PeerConnectionSource */ // defined in peer-connection-source.js /* global Pipeline */ // defined in pipeline.js /* global NullTransform, DropTransform, DelayTransform */ // defined in simple-transforms.js /* global VideoSink */ // defined in video-sink.js /* global VideoSource */ // defined in video-source.js /* global WebGLTransform */ // defined in webgl-transform.js /* global WebCodecTransform */ // defined in webcodec-transform.js /** * Allows inspecting objects in the console. See console log messages for * attributes added to this debug object. * @type {!Object} */ let debug = {}; /** * FrameTransformFn applies a transform to a frame and queues the output frame * (if any) using the controller. The first argument is the input frame and the * second argument is the stream controller. * The VideoFrame should be closed as soon as it is no longer needed to free * resources and maintain good performance. * @typedef {function( * !VideoFrame, * !TransformStreamDefaultController): !Promise} */ let FrameTransformFn; // eslint-disable-line no-unused-vars /** * Creates a pair of MediaStreamTrackProcessor and MediaStreamTrackGenerator * that applies transform to sourceTrack. This function is the core part of the * sample, demonstrating how to use the new API. * @param {!MediaStreamTrack} sourceTrack the video track to be transformed. The * track can be from any source, e.g. getUserMedia, RTCTrackEvent, or * captureStream on HTMLMediaElement or HTMLCanvasElement. * @param {!FrameTransformFn} transform the transform to apply to sourceTrack; * the transformed frames are available on the returned track. See the * implementations of FrameTransform.transform later in this file for * examples. * @param {!AbortSignal} signal can be used to stop processing * @return {!MediaStreamTrack} the result of sourceTrack transformed using * transform. */ // eslint-disable-next-line no-unused-vars function createProcessedMediaStreamTrack(sourceTrack, transform, signal) { // Create the MediaStreamTrackProcessor. /** @type {?MediaStreamTrackProcessor} */ let processor; try { processor = new MediaStreamTrackProcessor(sourceTrack); } catch (e) { alert(`MediaStreamTrackProcessor failed: ${e}`); throw e; } // Create the MediaStreamTrackGenerator. /** @type {?MediaStreamTrackGenerator} */ let generator; try { generator = new MediaStreamTrackGenerator('video'); } catch (e) { alert(`MediaStreamTrackGenerator failed: ${e}`); throw e; } const source = processor.readable; const sink = generator.writable; // Create a TransformStream using our FrameTransformFn. (Note that the // "Stream" in TransformStream refers to the Streams API, specified by // https://streams.spec.whatwg.org/, not the Media Capture and Streams API, // specified by https://w3c.github.io/mediacapture-main/.) /** @type {!TransformStream} */ const transformer = new TransformStream({transform}); // Apply the transform to the processor's stream and send it to the // generator's stream. const promise = source.pipeThrough(transformer, {signal}).pipeTo(sink); promise.catch((e) => { if (signal.aborted) { console.log( '[createProcessedMediaStreamTrack] Shutting down streams after abort.'); } else { console.error( '[createProcessedMediaStreamTrack] Error from stream transform:', e); } source.cancel(e); sink.abort(e); }); debug['processor'] = processor; debug['generator'] = generator; debug['transformStream'] = transformer; console.log( '[createProcessedMediaStreamTrack] Created MediaStreamTrackProcessor, ' + 'MediaStreamTrackGenerator, and TransformStream.', 'debug.processor =', processor, 'debug.generator =', generator, 'debug.transformStream =', transformer); return generator; } /** * The current video pipeline. Initialized by initPipeline(). * @type {?Pipeline} */ let pipeline; /** * Sets up handlers for interacting with the UI elements on the page. */ function initUI() { const sourceSelector = /** @type {!HTMLSelectElement} */ ( document.getElementById('sourceSelector')); const sourceVisibleCheckbox = (/** @type {!HTMLInputElement} */ ( document.getElementById('sourceVisible'))); /** * Updates the pipeline based on the current settings of the sourceSelector * and sourceVisible UI elements. Unlike updatePipelineSource(), never * re-initializes the pipeline. */ function updatePipelineSourceIfSet() { const sourceType = sourceSelector.options[sourceSelector.selectedIndex].value; if (!sourceType) return; console.log(`[UI] Selected source: ${sourceType}`); let source; switch (sourceType) { case 'camera': source = new CameraSource(); break; case 'video': source = new VideoSource(); break; case 'canvas': source = new CanvasSource(); break; case 'pc': source = new PeerConnectionSource(new CameraSource()); break; default: alert(`unknown source ${sourceType}`); return; } source.setVisibility(sourceVisibleCheckbox.checked); pipeline.updateSource(source); } /** * Updates the pipeline based on the current settings of the sourceSelector * and sourceVisible UI elements. If the "stopped" option is selected, * reinitializes the pipeline instead. */ function updatePipelineSource() { const sourceType = sourceSelector.options[sourceSelector.selectedIndex].value; if (!sourceType || !pipeline) { initPipeline(); } else { updatePipelineSourceIfSet(); } } sourceSelector.oninput = updatePipelineSource; sourceSelector.disabled = false; /** * Updates the source visibility, if the source is already started. */ function updatePipelineSourceVisibility() { console.log(`[UI] Changed source visibility: ${ sourceVisibleCheckbox.checked ? 'added' : 'removed'}`); if (pipeline) { const source = pipeline.getSource(); if (source) { source.setVisibility(sourceVisibleCheckbox.checked); } } } sourceVisibleCheckbox.oninput = updatePipelineSourceVisibility; sourceVisibleCheckbox.disabled = false; const transformSelector = /** @type {!HTMLSelectElement} */ ( document.getElementById('transformSelector')); /** * Updates the pipeline based on the current settings of the transformSelector * UI element. */ function updatePipelineTransform() { if (!pipeline) { return; } const transformType = transformSelector.options[transformSelector.selectedIndex].value; console.log(`[UI] Selected transform: ${transformType}`); switch (transformType) { case 'webgl': pipeline.updateTransform(new WebGLTransform()); break; case 'canvas2d': pipeline.updateTransform(new CanvasTransform()); break; case 'drop': // Defined in simple-transforms.js. pipeline.updateTransform(new DropTransform()); break; case 'noop': // Defined in simple-transforms.js. pipeline.updateTransform(new NullTransform()); break; case 'delay': // Defined in simple-transforms.js. pipeline.updateTransform(new DelayTransform()); break; case 'webcodec': // Defined in webcodec-transform.js pipeline.updateTransform(new WebCodecTransform()); break; default: alert(`unknown transform ${transformType}`); break; } } transformSelector.oninput = updatePipelineTransform; transformSelector.disabled = false; const sinkSelector = (/** @type {!HTMLSelectElement} */ ( document.getElementById('sinkSelector'))); /** * Updates the pipeline based on the current settings of the sinkSelector UI * element. */ function updatePipelineSink() { const sinkType = sinkSelector.options[sinkSelector.selectedIndex].value; console.log(`[UI] Selected sink: ${sinkType}`); switch (sinkType) { case 'video': pipeline.updateSink(new VideoSink()); break; case 'pc': pipeline.updateSink(new PeerConnectionSink()); break; default: alert(`unknown sink ${sinkType}`); break; } } sinkSelector.oninput = updatePipelineSink; sinkSelector.disabled = false; /** * Initializes/reinitializes the pipeline. Called on page load and after the * user chooses to stop the video source. */ function initPipeline() { if (pipeline) pipeline.destroy(); pipeline = new Pipeline(); debug = {pipeline}; updatePipelineSourceIfSet(); updatePipelineTransform(); updatePipelineSink(); console.log( '[initPipeline] Created new Pipeline.', 'debug.pipeline =', pipeline); } } window.onload = initUI; ================================================ FILE: src/content/insertable-streams/video-processing/js/peer-connection-pipe.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /** * Sends a MediaStream to one end of an RTCPeerConnection and provides the * remote end as the resulting MediaStream. * In an actual video calling app, the two RTCPeerConnection objects would be * instantiated on different devices. However, in this sample, both sides of the * peer connection are local to allow the sample to be self-contained. * For more detailed samples using RTCPeerConnection, take a look at * https://webrtc.github.io/samples/. */ class PeerConnectionPipe { // eslint-disable-line no-unused-vars /** * @param {!MediaStream} inputStream stream to pipe over the peer connection * @param {string} debugPath the path to this object from the debug global var */ constructor(inputStream, debugPath) { /** * @private @const {!RTCPeerConnection} the calling side of the peer * connection, connected to inputStream_. */ this.caller_ = new RTCPeerConnection(null); /** * @private @const {!RTCPeerConnection} the answering side of the peer * connection, providing the stream returned by getMediaStream. */ this.callee_ = new RTCPeerConnection(null); /** @private {string} */ this.debugPath_ = debugPath; /** * @private @const {!Promise} the stream containing tracks * from callee_, returned by getMediaStream. */ this.outputStreamPromise_ = this.init_(inputStream); } /** * Sets the path to this object from the debug global var. * @param {string} path */ setDebugPath(path) { this.debugPath_ = path; } /** * @param {!MediaStream} inputStream stream to pipe over the peer connection * @return {!Promise} * @private */ async init_(inputStream) { console.log( '[PeerConnectionPipe] Initiating peer connection.', `${this.debugPath_} =`, this); this.caller_.onicecandidate = (/** !RTCPeerConnectionIceEvent*/ event) => { if (event.candidate) this.callee_.addIceCandidate(event.candidate); }; this.callee_.onicecandidate = (/** !RTCPeerConnectionIceEvent */ event) => { if (event.candidate) this.caller_.addIceCandidate(event.candidate); }; const outputStream = new MediaStream(); const receiverStreamPromise = new Promise(resolve => { this.callee_.ontrack = (/** !RTCTrackEvent */ event) => { outputStream.addTrack(event.track); if (outputStream.getTracks().length == inputStream.getTracks().length) { resolve(outputStream); } }; }); inputStream.getTracks().forEach(track => { this.caller_.addTransceiver(track, {direction: 'sendonly'}); }); await this.caller_.setLocalDescription(); await this.callee_.setRemoteDescription( /** @type {!RTCSessionDescription} */ (this.caller_.localDescription)); await this.callee_.setLocalDescription(); await this.caller_.setRemoteDescription( /** @type {!RTCSessionDescription} */ (this.callee_.localDescription)); await receiverStreamPromise; console.log( '[PeerConnectionPipe] Peer connection established.', `${this.debugPath_}.caller_ =`, this.caller_, `${this.debugPath_}.callee_ =`, this.callee_); return receiverStreamPromise; } /** * Provides the MediaStream that has been piped through a peer connection. * @return {!Promise} */ getOutputStream() { return this.outputStreamPromise_; } /** Frees any resources used by this object. */ destroy() { console.log('[PeerConnectionPipe] Closing peer connection.'); this.caller_.close(); this.callee_.close(); } } ================================================ FILE: src/content/insertable-streams/video-processing/js/peer-connection-sink.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /* global PeerConnectionPipe */ // defined in peer-connection-pipe.js /* global VideoSink */ // defined in video-sink.js /** * Sends the transformed video to one end of an RTCPeerConnection and displays * the remote end in a video element. In this sample, a PeerConnectionSink * represents processing the local user's camera input using a * MediaStreamTrackProcessor before sending it to a remote video call * participant. Contrast with a PeerConnectionSource. * @implements {MediaStreamSink} in pipeline.js */ class PeerConnectionSink { // eslint-disable-line no-unused-vars constructor() { /** * @private @const {!VideoSink} manages displaying the video stream in the * page */ this.videoSink_ = new VideoSink(); /** * @private {?PeerConnectionPipe} handles piping the MediaStream through an * RTCPeerConnection */ this.pipe_ = null; /** @private {string} */ this.debugPath_ = 'debug.pipeline.sink_'; this.videoSink_.setDebugPath(`${this.debugPath_}.videoSink_`); } /** @override */ async setMediaStream(stream) { console.log( '[PeerConnectionSink] Setting peer connection sink stream.', stream); if (this.pipe_) this.pipe_.destroy(); this.pipe_ = new PeerConnectionPipe(stream, `${this.debugPath_}.pipe_`); const pipedStream = await this.pipe_.getOutputStream(); console.log( '[PeerConnectionSink] Received callee peer connection stream.', pipedStream); await this.videoSink_.setMediaStream(pipedStream); } /** @override */ destroy() { this.videoSink_.destroy(); if (this.pipe_) this.pipe_.destroy(); } } ================================================ FILE: src/content/insertable-streams/video-processing/js/peer-connection-source.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /* global PeerConnectionPipe */ // defined in peer-connection-pipe.js /* global VideoMirrorHelper */ // defined in video-mirror-helper.js /** * Sends the original source video to one end of an RTCPeerConnection and * provides the remote end as the final source. * In this sample, a PeerConnectionSource represents receiving video from a * remote participant and locally processing it using a * MediaStreamTrackProcessor before displaying it on the screen. Contrast with a * PeerConnectionSink. * @implements {MediaStreamSource} in pipeline.js */ class PeerConnectionSource { // eslint-disable-line no-unused-vars /** * @param {!MediaStreamSource} originalSource original stream source, whose * output is sent over the peer connection */ constructor(originalSource) { /** * @private @const {!VideoMirrorHelper} manages displaying the video stream * in the page */ this.videoMirrorHelper_ = new VideoMirrorHelper(); /** * @private @const {!MediaStreamSource} original stream source, whose output * is sent on the sender peer connection. In an actual video calling * app, this stream would be generated from the remote participant's * camera. However, in this sample, both sides of the peer connection * are local to allow the sample to be self-contained. */ this.originalStreamSource_ = originalSource; /** * @private {?PeerConnectionPipe} handles piping the MediaStream through an * RTCPeerConnection */ this.pipe_ = null; /** @private {string} */ this.debugPath_ = ''; } /** @override */ setDebugPath(path) { this.debugPath_ = path; this.videoMirrorHelper_.setDebugPath(`${path}.videoMirrorHelper_`); this.originalStreamSource_.setDebugPath(`${path}.originalStreamSource_`); if (this.pipe_) this.pipe_.setDebugPath(`${path}.pipe_`); } /** @override */ setVisibility(visible) { this.videoMirrorHelper_.setVisibility(visible); } /** @override */ async getMediaStream() { if (this.pipe_) return this.pipe_.getOutputStream(); console.log( '[PeerConnectionSource] Obtaining original source media stream.', `${this.debugPath_}.originalStreamSource_ =`, this.originalStreamSource_); const originalStream = await this.originalStreamSource_.getMediaStream(); this.pipe_ = new PeerConnectionPipe(originalStream, `${this.debugPath_}.pipe_`); const outputStream = await this.pipe_.getOutputStream(); console.log( '[PeerConnectionSource] Received callee peer connection stream.', outputStream); this.videoMirrorHelper_.setStream(outputStream); return outputStream; } /** @override */ destroy() { this.videoMirrorHelper_.destroy(); if (this.pipe_) this.pipe_.destroy(); this.originalStreamSource_.destroy(); } } ================================================ FILE: src/content/insertable-streams/video-processing/js/pipeline.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /* global createProcessedMediaStreamTrack */ // defined in main.js /** * Wrapper around createProcessedMediaStreamTrack to apply transform to a * MediaStream. * @param {!MediaStream} sourceStream the video stream to be transformed. The * first video track will be used. * @param {!FrameTransformFn} transform the transform to apply to the * sourceStream. * @param {!AbortSignal} signal can be used to stop processing * @return {!MediaStream} holds a single video track of the transformed video * frames */ function createProcessedMediaStream(sourceStream, transform, signal) { // For this sample, we're only dealing with video tracks. /** @type {!MediaStreamTrack} */ const sourceTrack = sourceStream.getVideoTracks()[0]; const processedTrack = createProcessedMediaStreamTrack(sourceTrack, transform, signal); // Create a new MediaStream to hold our processed track. const processedStream = new MediaStream(); processedStream.addTrack(processedTrack); return processedStream; } /** * Interface implemented by all video sources the user can select. A common * interface allows the user to choose a source independently of the transform * and sink. * @interface */ class MediaStreamSource { // eslint-disable-line no-unused-vars /** * Sets the path to this object from the debug global var. * @param {string} path */ setDebugPath(path) {} /** * Indicates if the source video should be mirrored/displayed on the page. If * false (the default), any element producing frames will not be a child of * the document. * @param {boolean} visible whether to add the raw source video to the page */ setVisibility(visible) {} /** * Initializes and returns the MediaStream for this source. * @return {!Promise} */ async getMediaStream() {} /** Frees any resources used by this object. */ destroy() {} } /** * Interface implemented by all video transforms that the user can select. A * common interface allows the user to choose a transform independently of the * source and sink. * @interface */ class FrameTransform { // eslint-disable-line no-unused-vars /** Initializes state that is reused across frames. */ async init() {} /** * Applies the transform to frame. Queues the output frame (if any) using the * controller. * @param {!VideoFrame} frame the input frame * @param {!TransformStreamDefaultController} controller */ async transform(frame, controller) {} /** Frees any resources used by this object. */ destroy() {} } /** * Interface implemented by all video sinks that the user can select. A common * interface allows the user to choose a sink independently of the source and * transform. * @interface */ class MediaStreamSink { // eslint-disable-line no-unused-vars /** * @param {!MediaStream} stream */ async setMediaStream(stream) {} /** Frees any resources used by this object. */ destroy() {} } /** * Assembles a MediaStreamSource, FrameTransform, and MediaStreamSink together. */ class Pipeline { // eslint-disable-line no-unused-vars constructor() { /** @private {?MediaStreamSource} set by updateSource*/ this.source_ = null; /** @private {?FrameTransform} set by updateTransform */ this.frameTransform_ = null; /** @private {?MediaStreamSink} set by updateSink */ this.sink_ = null; /** @private {!AbortController} may used to stop all processing */ this.abortController_ = new AbortController(); /** * @private {?MediaStream} set in maybeStartPipeline_ after all of source_, * frameTransform_, and sink_ are set */ this.processedStream_ = null; } /** @return {?MediaStreamSource} */ getSource() { return this.source_; } /** * Sets a new source for the pipeline. * @param {!MediaStreamSource} mediaStreamSource */ async updateSource(mediaStreamSource) { if (this.source_) { this.abortController_.abort(); this.abortController_ = new AbortController(); this.source_.destroy(); this.processedStream_ = null; } this.source_ = mediaStreamSource; this.source_.setDebugPath('debug.pipeline.source_'); console.log( '[Pipeline] Updated source.', 'debug.pipeline.source_ = ', this.source_); await this.maybeStartPipeline_(); } /** @private */ async maybeStartPipeline_() { if (this.processedStream_ || !this.source_ || !this.frameTransform_ || !this.sink_) { return; } const sourceStream = await this.source_.getMediaStream(); await this.frameTransform_.init(); try { this.processedStream_ = createProcessedMediaStream( sourceStream, async (frame, controller) => { if (this.frameTransform_) { await this.frameTransform_.transform(frame, controller); } }, this.abortController_.signal); } catch (e) { this.destroy(); return; } await this.sink_.setMediaStream(this.processedStream_); console.log( '[Pipeline] Pipeline started.', 'debug.pipeline.abortController_ =', this.abortController_); } /** * Sets a new transform for the pipeline. * @param {!FrameTransform} frameTransform */ async updateTransform(frameTransform) { if (this.frameTransform_) this.frameTransform_.destroy(); this.frameTransform_ = frameTransform; console.log( '[Pipeline] Updated frame transform.', 'debug.pipeline.frameTransform_ = ', this.frameTransform_); if (this.processedStream_) { await this.frameTransform_.init(); } else { await this.maybeStartPipeline_(); } } /** * Sets a new sink for the pipeline. * @param {!MediaStreamSink} mediaStreamSink */ async updateSink(mediaStreamSink) { if (this.sink_) this.sink_.destroy(); this.sink_ = mediaStreamSink; console.log( '[Pipeline] Updated sink.', 'debug.pipeline.sink_ = ', this.sink_); if (this.processedStream_) { await this.sink_.setMediaStream(this.processedStream_); } else { await this.maybeStartPipeline_(); } } /** Frees any resources used by this object. */ destroy() { console.log('[Pipeline] Destroying Pipeline'); this.abortController_.abort(); if (this.source_) this.source_.destroy(); if (this.frameTransform_) this.frameTransform_.destroy(); if (this.sink_) this.sink_.destroy(); } } ================================================ FILE: src/content/insertable-streams/video-processing/js/simple-transforms.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /** * Does nothing. * @implements {FrameTransform} in pipeline.js */ class NullTransform { // eslint-disable-line no-unused-vars /** @override */ async init() {} /** @override */ async transform(frame, controller) { controller.enqueue(frame); } /** @override */ destroy() {} } /** * Drops frames at random. * @implements {FrameTransform} in pipeline.js */ class DropTransform { // eslint-disable-line no-unused-vars /** @override */ async init() {} /** @override */ async transform(frame, controller) { if (Math.random() < 0.5) { controller.enqueue(frame); } else { frame.close(); } } /** @override */ destroy() {} } /** * Delays all frames by 100ms. * @implements {FrameTransform} in pipeline.js */ class DelayTransform { // eslint-disable-line no-unused-vars /** @override */ async init() {} /** @override */ async transform(frame, controller) { await new Promise(resolve => setTimeout(resolve, 100)); controller.enqueue(frame); } /** @override */ destroy() {} } ================================================ FILE: src/content/insertable-streams/video-processing/js/video-mirror-helper.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /** * Helper to display a MediaStream in an HTMLVideoElement, based on the * visibility setting. */ class VideoMirrorHelper { // eslint-disable-line no-unused-vars constructor() { /** @private {boolean} */ this.visibility_ = false; /** @private {?MediaStream} the stream to display */ this.stream_ = null; /** * @private {?HTMLVideoElement} video element mirroring the camera stream. * Set if visibility_ is true and stream_ is set. */ this.video_ = null; /** @private {string} */ this.debugPath_ = ''; } /** * Sets the path to this object from the debug global var. * @param {string} path */ setDebugPath(path) { this.debugPath_ = path; } /** * Indicates if the video should be mirrored/displayed on the page. * @param {boolean} visible whether to add the video from the source stream to * the page */ setVisibility(visible) { this.visibility_ = visible; if (this.video_ && !this.visibility_) { this.video_.parentNode.removeChild(this.video_); this.video_ = null; } this.maybeAddVideoElement_(); } /** * @param {!MediaStream} stream */ setStream(stream) { this.stream_ = stream; this.maybeAddVideoElement_(); } /** @private */ maybeAddVideoElement_() { if (!this.video_ && this.visibility_ && this.stream_) { this.video_ = /** @type {!HTMLVideoElement} */ (document.createElement('video')); console.log( '[VideoMirrorHelper] Adding source video mirror.', `${this.debugPath_}.video_ =`, this.video_); this.video_.classList.add('video', 'sourceVideo'); this.video_.srcObject = this.stream_; const outputVideoContainer = document.getElementById('outputVideoContainer'); outputVideoContainer.parentNode.insertBefore( this.video_, outputVideoContainer); this.video_.play(); } } /** Frees any resources used by this object. */ destroy() { if (this.video_) { this.video_.pause(); this.video_.srcObject = null; this.video_.parentNode.removeChild(this.video_); } } } ================================================ FILE: src/content/insertable-streams/video-processing/js/video-sink.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /** * Displays the output stream in a video element. * @implements {MediaStreamSink} in pipeline.js */ class VideoSink { // eslint-disable-line no-unused-vars constructor() { /** * @private {?HTMLVideoElement} output video element */ this.video_ = null; /** @private {string} */ this.debugPath_ = 'debug.pipeline.sink_'; } /** * Sets the path to this object from the debug global var. * @param {string} path */ setDebugPath(path) { this.debugPath_ = path; } /** @override */ async setMediaStream(stream) { console.log('[VideoSink] Setting sink stream.', stream); if (!this.video_) { this.video_ = /** @type {!HTMLVideoElement} */ (document.createElement('video')); this.video_.classList.add('video', 'sinkVideo'); document.getElementById('outputVideoContainer').appendChild(this.video_); console.log( '[VideoSink] Added video element to page.', `${this.debugPath_}.video_ =`, this.video_); } this.video_.srcObject = stream; this.video_.play(); } /** @override */ destroy() { if (this.video_) { console.log('[VideoSink] Stopping sink video'); this.video_.pause(); this.video_.srcObject = null; this.video_.parentNode.removeChild(this.video_); } } } ================================================ FILE: src/content/insertable-streams/video-processing/js/video-source.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /** * Decodes and plays a video. * @implements {MediaStreamSource} in pipeline.js */ class VideoSource { // eslint-disable-line no-unused-vars constructor() { /** @private {boolean} */ this.visibility_ = false; /** @private {?HTMLVideoElement} video element providing the MediaStream */ this.video_ = null; /** * @private {?Promise} a Promise that resolves to the * MediaStream from captureStream. Set iff video_ is set. */ this.stream_ = null; /** @private {string} */ this.debugPath_ = ''; } /** @override */ setDebugPath(path) { this.debugPath_ = path; } /** @override */ setVisibility(visible) { this.visibility_ = visible; if (this.video_) { this.updateVideoVisibility(); } } /** @private */ updateVideoVisibility() { if (this.video_.parentNode && !this.visibility_) { if (!this.video_.paused) { // Video playback is automatically paused when the element is removed // from the DOM. That is not the behavior we want. this.video_.onpause = async () => { this.video_.onpause = null; await this.video_.play(); }; } this.video_.parentNode.removeChild(this.video_); } else if (!this.video_.parentNode && this.visibility_) { console.log( '[VideoSource] Adding source video element to page.', `${this.debugPath_}.video_ =`, this.video_); const outputVideoContainer = document.getElementById('outputVideoContainer'); outputVideoContainer.parentNode.insertBefore( this.video_, outputVideoContainer); } } /** @override */ async getMediaStream() { if (this.stream_) return this.stream_; console.log('[VideoSource] Loading video'); this.video_ = /** @type {!HTMLVideoElement} */ (document.createElement('video')); this.video_.classList.add('video', 'sourceVideo'); this.video_.controls = true; this.video_.loop = true; this.video_.muted = true; // All browsers that support insertable streams also support WebM/VP8. this.video_.src = '../../../video/chrome.webm'; this.video_.load(); this.video_.play(); this.updateVideoVisibility(); this.stream_ = new Promise((resolve, reject) => { this.video_.oncanplay = () => { if (!resolve || !reject) return; console.log('[VideoSource] Obtaining video capture stream'); if (this.video_.captureStream) { resolve(this.video_.captureStream()); } else if (this.video_.mozCaptureStream) { resolve(this.video_.mozCaptureStream()); } else { const e = new Error('Stream capture is not supported'); console.error(e); reject(e); } resolve = null; reject = null; }; }); await this.stream_; console.log( '[VideoSource] Received source video stream.', `${this.debugPath_}.stream_ =`, this.stream_); return this.stream_; } /** @override */ destroy() { if (this.video_) { console.log('[VideoSource] Stopping source video'); this.video_.pause(); if (this.video_.parentNode) { this.video_.parentNode.removeChild(this.video_); } } } } ================================================ FILE: src/content/insertable-streams/video-processing/js/webcodec-transform.js ================================================ /* * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /** * Encodes and decodes frames using the WebCodec API. * @implements {FrameTransform} in pipeline.js */ class WebCodecTransform { // eslint-disable-line no-unused-vars constructor() { // Encoder and decoder are initialized in init() this.decoder_ = null; this.encoder_ = null; this.controller_ = null; } /** @override */ async init() { console.log('[WebCodecTransform] Initializing encoder and decoder'); this.decoder_ = new VideoDecoder({ output: frame => this.handleDecodedFrame(frame), error: this.error }); this.encoder_ = new VideoEncoder({ output: frame => this.handleEncodedFrame(frame), error: this.error }); this.encoder_.configure({codec: 'vp8', width: 640, height: 480}); this.decoder_.configure({codec: 'vp8', width: 640, height: 480}); } /** @override */ async transform(frame, controller) { if (!this.encoder_) { frame.close(); return; } try { this.controller_ = controller; this.encoder_.encode(frame); } finally { frame.close(); } } /** @override */ destroy() {} /* Helper functions */ handleEncodedFrame(encodedFrame) { this.decoder_.decode(encodedFrame); } handleDecodedFrame(videoFrame) { if (!this.controller_) { videoFrame.close(); return; } this.controller_.enqueue(videoFrame); } error(e) { console.log('[WebCodecTransform] Bad stuff happened: ' + e); } } ================================================ FILE: src/content/insertable-streams/video-processing/js/webgl-transform.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /** * Applies a warp effect using WebGL. * @implements {FrameTransform} in pipeline.js */ class WebGLTransform { // eslint-disable-line no-unused-vars constructor() { // All fields are initialized in init() /** @private {?OffscreenCanvas} canvas used to create the WebGL context */ this.canvas_ = null; /** @private {?WebGLRenderingContext} */ this.gl_ = null; /** @private {?WebGLUniformLocation} location of inSampler */ this.sampler_ = null; /** @private {?WebGLProgram} */ this.program_ = null; /** @private {?WebGLTexture} input texture */ this.texture_ = null; /** @private {string} */ this.debugPath_ = 'debug.pipeline.frameTransform_'; } /** @override */ async init() { console.log('[WebGLTransform] Initializing WebGL.'); this.canvas_ = new OffscreenCanvas(1, 1); const gl = /** @type {?WebGLRenderingContext} */ ( this.canvas_.getContext('webgl')); if (!gl) { alert( 'Failed to create WebGL context. Check that WebGL is supported ' + 'by your browser and hardware.'); return; } this.gl_ = gl; const vertexShader = this.loadShader_(gl.VERTEX_SHADER, ` precision mediump float; attribute vec3 g_Position; attribute vec2 g_TexCoord; varying vec2 texCoord; void main() { gl_Position = vec4(g_Position, 1.0); texCoord = g_TexCoord; }`); const fragmentShader = this.loadShader_(gl.FRAGMENT_SHADER, ` precision mediump float; varying vec2 texCoord; uniform sampler2D inSampler; void main(void) { float boundary = distance(texCoord, vec2(0.5)) - 0.2; if (boundary < 0.0) { gl_FragColor = texture2D(inSampler, texCoord); } else { // Rotate the position float angle = 2.0 * boundary; vec2 rotation = vec2(sin(angle), cos(angle)); vec2 fromCenter = texCoord - vec2(0.5); vec2 rotatedPosition = vec2( fromCenter.x * rotation.y + fromCenter.y * rotation.x, fromCenter.y * rotation.y - fromCenter.x * rotation.x) + vec2(0.5); gl_FragColor = texture2D(inSampler, rotatedPosition); } }`); if (!vertexShader || !fragmentShader) return; // Create the program object const programObject = gl.createProgram(); gl.attachShader(programObject, vertexShader); gl.attachShader(programObject, fragmentShader); // Link the program gl.linkProgram(programObject); // Check the link status const linked = gl.getProgramParameter(programObject, gl.LINK_STATUS); if (!linked) { const infoLog = gl.getProgramInfoLog(programObject); gl.deleteProgram(programObject); throw new Error(`Error linking program:\n${infoLog}`); } gl.deleteShader(vertexShader); gl.deleteShader(fragmentShader); this.sampler_ = gl.getUniformLocation(programObject, 'inSampler'); this.program_ = programObject; // Bind attributes const vertices = [1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0]; // Pass-through. const txtcoords = [1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0]; // Mirror horizonally. // const txtcoords = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0]; this.attributeSetFloats_('g_Position', 2, vertices); this.attributeSetFloats_('g_TexCoord', 2, txtcoords); // Initialize input texture this.texture_ = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.texture_); const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixel); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); console.log( '[WebGLTransform] WebGL initialized.', `${this.debugPath_}.canvas_ =`, this.canvas_, `${this.debugPath_}.gl_ =`, this.gl_); } /** * Creates and compiles a WebGLShader from the provided source code. * @param {number} type either VERTEX_SHADER or FRAGMENT_SHADER * @param {string} shaderSrc * @return {!WebGLShader} * @private */ loadShader_(type, shaderSrc) { const gl = this.gl_; const shader = gl.createShader(type); // Load the shader source gl.shaderSource(shader, shaderSrc); // Compile the shader gl.compileShader(shader); // Check the compile status if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { const infoLog = gl.getShaderInfoLog(shader); gl.deleteShader(shader); throw new Error(`Error compiling shader:\n${infoLog}`); } return shader; } /** * Sets a floating point shader attribute to the values in arr. * @param {string} attrName the name of the shader attribute to set * @param {number} vsize the number of components of the shader attribute's * type * @param {!Array} arr the values to set * @private */ attributeSetFloats_(attrName, vsize, arr) { const gl = this.gl_; gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arr), gl.STATIC_DRAW); const attr = gl.getAttribLocation(this.program_, attrName); gl.enableVertexAttribArray(attr); gl.vertexAttribPointer(attr, vsize, gl.FLOAT, false, 0, 0); } /** @override */ async transform(frame, controller) { const gl = this.gl_; if (!gl || !this.canvas_) { frame.close(); return; } const width = frame.displayWidth; const height = frame.displayHeight; if (this.canvas_.width !== width || this.canvas_.height !== height) { this.canvas_.width = width; this.canvas_.height = height; gl.viewport(0, 0, width, height); } const timestamp = frame.timestamp; gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.texture_); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, frame); frame.close(); gl.useProgram(this.program_); gl.uniform1i(this.sampler_, 0); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); gl.bindTexture(gl.TEXTURE_2D, null); // alpha: 'discard' is needed in order to send frames to a PeerConnection. controller.enqueue(new VideoFrame(this.canvas_, {timestamp, alpha: 'discard'})); } /** @override */ destroy() { if (this.gl_) { console.log('[WebGLTransform] Forcing WebGL context to be lost.'); /** @type {!WEBGL_lose_context} */ ( this.gl_.getExtension('WEBGL_lose_context')) .loseContext(); } } } ================================================ FILE: src/content/insertable-streams/webgpu/css/main.css ================================================ /* * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ video { width: 480px; height: 270px; } .output { width: 960px; height: 540px; margin: 0px 0px 0px 0px; } .error { font-size: 20px; color:red; } ================================================ FILE: src/content/insertable-streams/webgpu/index.html ================================================ Integrations with WebGPU for custom video rendering

WebRTC samples Integrations with WebGPU for custom video rendering

This sample shows how to render multiple video streams to canvas using the insertable streams and WebGPU APIs. There are options to either process the rendering on the main thread or on a worker thread.


Choose type of rendering:
Input:
Output:

Note: This sample is using WebGPU API that is in Origin Trial as of 2021-09-21 and is available in Chrome M94 if the experimental code is enabled on the command line with --enable-unsafe-webgpu.

View source on GitHub
================================================ FILE: src/content/insertable-streams/webgpu/js/main.js ================================================ 'use strict'; /* global MediaStreamTrackProcessor, MediaStreamTrackGenerator */ if (typeof MediaStreamTrackProcessor === 'undefined' || typeof MediaStreamTrackGenerator === 'undefined') { const errorMessage = 'Your browser does not support the MediaStreamTrack ' + 'API for Insertable Streams of Media which was shipped in M94.'; document.getElementById('errorMsg').innerText = errorMessage; console.log(errorMessage); } /* global WebGPUTransform */ // defined in multi_video_main.js /* global WebGPUWorker */ // defined in multi_video_worker_manager.js let videoElement; async function getMediaStream(src) { videoElement = document.getElementById('inputVideo'); videoElement.controls = true; videoElement.loop = true; videoElement.muted = true; videoElement.src = src; videoElement.load(); videoElement.play(); let sourceStream; const mediaPromise = new Promise((resolve, reject) => { videoElement.oncanplay = () => { if (!resolve || !reject) return; console.log('Obtaining video capture stream'); if (videoElement.captureStream) { sourceStream = videoElement.captureStream(); resolve(); } else if (videoElement.mozCaptureStream) { sourceStream = videoElement.mozCaptureStream(); resolve(); } else { reject(new Error('Stream capture is not supported')); } resolve = null; reject = null; }; }); await mediaPromise; console.log( 'Received source video stream.', sourceStream); return sourceStream; } function getUserMediaStream() { return navigator.mediaDevices.getUserMedia({ audio: false, video: {width: 480, height: 270} }).catch(err => { throw new Error('Unable to fetch getUserMedia stream ' + err); }); } let gpuTransform; let gumTrack; let gumVideo; async function main(sourceType) { const gumStream = await getUserMediaStream(); gumTrack = gumStream.getVideoTracks()[0]; const gumProcessor = new MediaStreamTrackProcessor({track: gumTrack}); gumVideo = document.getElementById('gumInputVideo'); gumVideo.srcObject = gumStream; gumVideo.play(); const videoStream = await getMediaStream('../../../video/chrome.webm'); const videoTrack = videoStream.getVideoTracks()[0]; const videoProcessor = new MediaStreamTrackProcessor({track: videoTrack}); if (sourceType === 'main') { gpuTransform = new WebGPUTransform(); } if (sourceType === 'worker') { gpuTransform = new WebGPUWorker(); } await gpuTransform.init(); await gpuTransform.transform(videoProcessor.readable, gumProcessor.readable); } function destroy_source() { if (videoElement) { console.log('Stopping source video'); videoElement.pause(); } if (gumVideo) { console.log('Stopping gUM stream'); gumVideo.pause(); gumVideo.srcObject = null; } if (gumTrack) gumTrack.stop(); } const sourceSelector = document.getElementById('sourceSelector'); function updateSource() { if (gpuTransform) { gpuTransform.destroy(); } gpuTransform = null; destroy_source(); const sourceType = sourceSelector.options[sourceSelector.selectedIndex].value; console.log('New source is', sourceType); if (sourceType !== 'stopped') { main(sourceType); } } sourceSelector.oninput = updateSource; ================================================ FILE: src/content/insertable-streams/webgpu/js/multi_video_main.js ================================================ 'use strict'; const wgslShaders = { vertex: ` struct VertexInput { [[location(0)]] position : vec3; [[location(1)]] uv : vec2; }; struct VertexOutput { [[builtin(position)]] Position : vec4; [[location(0)]] fragUV : vec2; }; [[stage(vertex)]] fn main(input : VertexInput) -> VertexOutput { var output : VertexOutput; output.Position = vec4(input.position, 1.0); output.fragUV = vec2(-0.5,-0.0) + input.uv; return output; } `, fragment: ` [[binding(0), group(0)]] var mySampler: sampler; [[binding(1), group(0)]] var myTexture: texture_2d; [[stage(fragment)]] fn main([[location(0)]] fragUV : vec2) -> [[location(0)]] vec4 { return textureSample(myTexture, mySampler, fragUV); } `, }; class WebGPUTransform { // eslint-disable-line no-unused-vars constructor() { this.canvas_ = null; this.context_ = null; this.device_ = null; this.renderPipeline_ = null; this.sampler_ = null; this.videoTexture_ = null; this.vertexBuffer_ = null; } async init(inputCanvas) { console.log('[WebGPUTransform] Initializing WebGPU.'); this.canvas_ = inputCanvas; let errorElement; if (!this.canvas_) { this.canvas_ = document.createElement('canvas'); document.getElementById('outputVideo').append(this.canvas_); this.canvas_.width = 960; this.canvas_.height = 540; errorElement = document.getElementById('errorMsg'); } const canvas = this.canvas_; const context = canvas.getContext('webgpu'); if (!context) { const errorMessage = 'Your browser does not support the WebGPU API.' + ' Please see the note at the bottom of the page.'; if (errorElement) errorElement.innerText = errorMessage; return errorMessage; } this.context_ = context; const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); this.device_ = device; if (!this.device_) { console.log('[WebGPUTransform] requestDevice failed.'); return; } const swapChainFormat = 'bgra8unorm'; const rectVerts = new Float32Array([ 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, -1.0, -1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, -1.0, -1.0, 0.0, 0.0, 1.0, -1.0, 1.0, 0.0, 0.0, 0.0, ]); // Creates a GPU buffer. const vertexBuffer = device.createBuffer({ size: rectVerts.byteLength, usage: GPUBufferUsage.VERTEX, mappedAtCreation: true, }); // Copies rectVerts to vertexBuffer new Float32Array(vertexBuffer.getMappedRange()).set(rectVerts); vertexBuffer.unmap(); this.vertexBuffer_ = vertexBuffer; context.configure({ device, format: swapChainFormat }); this.renderPipeline_ = device.createRenderPipeline({ vertex: { module: device.createShaderModule({ code: wgslShaders.vertex, }), entryPoint: 'main', buffers: [ { arrayStride: 20, attributes: [ { // position shaderLocation: 0, offset: 0, format: 'float32x3', }, { // uv shaderLocation: 1, offset: 12, format: 'float32x2', }, ], }, ], }, fragment: { module: device.createShaderModule({ code: wgslShaders.fragment, }), entryPoint: 'main', targets: [ { format: swapChainFormat, }, ], }, primitive: { topology: 'triangle-list', }, }); this.videoTexture_ = device.createTexture({ size: [480 * 2, 270 * 2], format: 'rgba8unorm', usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT, }); this.sampler_ = device.createSampler({ addressModeU: 'repeat', addressModeV: 'repeat', addressModeW: 'repeat', magFilter: 'linear', minFilter: 'linear', }); } async copyOnTexture(device, videoTexture, frame, xcorr, ycorr) { if (!frame) { return; } // Using GPUExternalTexture(when it's implemented for Breakout Box frames) will // avoid making extra copies through ImageBitmap. const videoBitmap = await createImageBitmap(frame, {resizeWidth: 480, resizeHeight: 270}); device.queue.copyExternalImageToTexture( {source: videoBitmap, origin: {x: 0, y: 0}}, {texture: videoTexture, origin: {x: xcorr, y: ycorr}}, { // the width of the image being copied width: videoBitmap.width, height: videoBitmap.height, } ); videoBitmap.close(); frame.close(); } async renderOnScreen(videoSource, gumSource) { const device = this.device_; const videoTexture = this.videoTexture_; if (!device) { console.log('[WebGPUTransform] device is undefined or null.'); return false; } const videoPromise = videoSource.read().then(({value}) => { this.copyOnTexture(device, videoTexture, value, 0, 270); }); const gumPromise = gumSource.read().then(({value}) => { this.copyOnTexture(device, videoTexture, value, 480, 0); }); await Promise.all([videoPromise, gumPromise]); if (!this.device_) { console.log('Check if destroy has been called asynchronously.'); return false; } const uniformBindGroup = device.createBindGroup({ layout: this.renderPipeline_.getBindGroupLayout(0), entries: [ { binding: 0, resource: this.sampler_, }, { binding: 1, resource: videoTexture.createView(), }, ], }); const commandEncoder = device.createCommandEncoder(); const textureView = this.context_.getCurrentTexture().createView(); const renderPassDescriptor = { colorAttachments: [ { view: textureView, loadValue: {r: 0.0, g: 0.0, b: 0.0, a: 1.0}, storeOp: 'store', }, ], }; const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(this.renderPipeline_); passEncoder.setVertexBuffer(0, this.vertexBuffer_); passEncoder.setBindGroup(0, uniformBindGroup); passEncoder.draw(6, 1, 0, 0); passEncoder.endPass(); device.queue.submit([commandEncoder.finish()]); return true; } async transform(videoStream, gumStream) { const videoSource = videoStream.getReader(); const gumSource = gumStream.getReader(); while (true) { const rendered = await this.renderOnScreen(videoSource, gumSource); if (!rendered) { break; } } videoSource.cancel(); gumSource.cancel(); } destroy() { if (this.device_) { // Currently being implemented. // await this.device_.destroy(); this.device_ = null; this.vertexBuffer_.destroy(); this.videoTexture_.destroy(); if (this.canvas_.parentNode) { this.canvas_.parentNode.removeChild(this.canvas_); } console.log('[WebGPUTransform] Context destroyed.',); } } } ================================================ FILE: src/content/insertable-streams/webgpu/js/multi_video_worker.js ================================================ importScripts('./multi_video_main.js'); 'use strict'; let mainTransform = null; /* global WebGPUTransform */ // defined in multi_video_main.js onmessage = async (event) => { const {operation} = event.data; if (operation === 'init') { mainTransform = new WebGPUTransform(); const {canvas} = event.data; const msg = await mainTransform.init(canvas); if (msg) { postMessage({error: msg}); } else { postMessage({result: 'Done'}); } } else if (operation === 'transform') { const {videoStream, gumStream} = event.data; mainTransform.transform(videoStream, gumStream); } }; ================================================ FILE: src/content/insertable-streams/webgpu/js/multi_video_worker_manager.js ================================================ 'use strict'; let worker; let screenCanvas; // eslint-disable-next-line no-unused-vars class WebGPUWorker { async init() { screenCanvas = document.createElement('canvas'); document.getElementById('outputVideo').append(screenCanvas); screenCanvas.width = 960; screenCanvas.height = 540; worker = new Worker('./js/multi_video_worker.js'); console.log('Created a worker thread.'); const offScreen = screenCanvas.transferControlToOffscreen(); const onMessage = new Promise((resolve, reject) => { worker.addEventListener('message', function handleMsgFromWorker(msg) { if (msg.data.error) { document.getElementById('errorMsg').innerText = msg.data.error; reject(msg.data.error); } if (msg.data.result === 'Done') { resolve(); } }); }); worker.postMessage( { operation: 'init', canvas: offScreen, }, [offScreen]); await onMessage; } transform(videoStream, gumStream) { if (videoStream && gumStream) { worker.postMessage( { operation: 'transform', videoStream: videoStream, gumStream: gumStream, }, [videoStream, gumStream]); } } destroy() { if (screenCanvas.parentNode) { screenCanvas.parentNode.removeChild(screenCanvas); } worker.terminate(); console.log('Worker thread destroyed.'); } } ================================================ FILE: src/content/peerconnection/always-negotiate-datachannels/css/main.css ================================================ /* * Copyright (c) 2026 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 0 0; width: 90px; } button#hangupButton { margin: 0; } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); margin: 0 0 20px 0; vertical-align: top; } video#localVideo { margin: 0 20px 20px 0; } @media screen and (max-width: 400px) { button { width: 83px; margin: 0 11px 10px 0; } video { height: 90px; margin: 0 0 10px 0; width: calc(50% - 7px); } video#localVideo { margin: 0 10px 20px 0; } } ================================================ FILE: src/content/peerconnection/always-negotiate-datachannels/index.html ================================================ Peer connection - Always negotiate datachannels

WebRTC samples Peer connection

Data channels are not used in this example but negotiating them in the SDP (using the checkbox in supported browsers) avoids video freezes (on the right video) if the alwaysNegotiateDataChannels configuration option is used.

View the console to see logging. The MediaStream object localStream, and the RTCPeerConnection objects pc1 and pc2 are in global scope, so you can inspect them in the console as well.

For more information about RTCPeerConnection, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/peerconnection/always-negotiate-datachannels/js/main.js ================================================ /* * Copyright (c) 2026 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); const renegotiateButton = document.getElementById('renegotiateButton'); const hangupButton = document.getElementById('hangupButton'); const negotiateCheckbox = document.getElementById('negotiateDataChannel'); startButton.onclick = start; callButton.onclick = call; renegotiateButton.onclick = renegotiate; hangupButton.onclick = hangup; let localStream; let pc1; let pc2; function log(text) { document.getElementById('log').innerText += text + '\n'; } function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } async function start() { startButton.disabled = true; negotiateCheckbox.disabled = true; const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true, }); localVideo.srcObject = stream; localStream = stream; callButton.disabled = false; negotiateCheckbox.disabled = false; } async function call() { callButton.disabled = true; negotiateCheckbox.disabled = true; renegotiateButton.disabled = false; hangupButton.disabled = false; console.log('Starting call'); const audioTracks = localStream.getAudioTracks(); if (audioTracks.length > 0) { console.log(`Using audio device: ${audioTracks[0].label}`); } const config = { alwaysNegotiateDataChannels: negotiateCheckbox.checked, }; pc1 = new RTCPeerConnection(config); // Use pc.getConfiguration to detect whether the feature is supported. if (pc1.getConfiguration().alwaysNegotiateDataChannels === undefined) { log('RTCConfiguration.alwaysNegotiateDataChannel is not supported in this browser (' + adapter.browserDetails.browser + '/' + adapter.browserDetails.version + ')'); } else { log('RTCConfiguration.alwaysNegotiateDataChannel is supported in this browser (' + adapter.browserDetails.browser + '/' + adapter.browserDetails.version + ')'); } pc1.onicecandidate = e => onIceCandidate(pc1, e); pc2 = new RTCPeerConnection(); pc2.onicecandidate = e => onIceCandidate(pc2, e); pc2.ontrack = gotRemoteStream; // Create an audio transceiver which serves as transport. pc1.addTransceiver('audio'); localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); await pc1.setLocalDescription(); await pc2.setRemoteDescription(pc1.localDescription); await pc2.setLocalDescription(); await pc1.setRemoteDescription(pc2.localDescription); renegotiateButton.disabled = false; } function gotRemoteStream(e) { remoteVideo.srcObject = e.streams[0]; } function onIceCandidate(pc, event) { getOtherPc(pc) .addIceCandidate(event.candidate) .catch(err => onAddIceCandidateError(pc, err)); } function onAddIceCandidateError(pc, error) { console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`); } async function renegotiate() { renegotiateButton.disabled = true; hangupButton.disabled = true; pc1.getTransceivers()[0].stop(); // stops the first transceiver. if (negotiateCheckbox.checked) { log('Stopped transceiver, the right video should not freeze (if always negotiating data channels is supported)'); } else { log('Stopped transceiver, the right video will freeze for ~4 seconds'); } await pc1.setLocalDescription(); await pc2.setRemoteDescription(pc1.localDescription); await new Promise(r => setTimeout(r, 4000)); // wait 4 seconds. console.log('renegotiated'); await pc2.setLocalDescription(); await pc1.setRemoteDescription(pc2.localDescription); await new Promise(r => setTimeout(r, 1000)); // wait 1 seconds. const stats = await pc2.getStats(); const freezes = [...stats.values()].filter(s => s.type === 'inbound-rtp' && s.kind === 'video').map(s => s.totalFreezesDuration); log('Renegotiation done, the getStats() API detected a freeze lasting ' + freezes + ' seconds'); log('Flip the "always negotiate data channels" box and try again'); hangupButton.disabled = false; } function hangup() { console.log('Ending call'); pc1.close(); pc2.close(); pc1 = null; pc2 = null; hangupButton.disabled = true; callButton.disabled = false; negotiateCheckbox.disabled = false; } ================================================ FILE: src/content/peerconnection/audio/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ audio { display: inline-block; position: relative; top: 9px; width: calc(100% - 120px); } button { margin: 0 20px 0 0; width: 96px; } table { border-collapse: collapse; } th, td { border: 1px solid black; } tr:hover { background-color: #f5f5f5; } div#audio { margin: 0 0 29px 0; } div#audio > div { margin: 0 0 20px 0; } div.label { display: inline-block; font-weight: 400; width: 120px; } div.graph-container { float: left; margin: 0.5em; width: calc(50% - 1em); } a#viewSource { clear: both; } ================================================ FILE: src/content/peerconnection/audio/index.html ================================================ Peer connection: audio only

WebRTC samples Peer connection: audio only

Local audio:
Remote audio:
Bitrate
Packets sent per second
average audio level ([0..1])
View source on GitHub
Bitrate and Packes sent per second - approximate results in browsers
Opus iSAC 16K G722 PCMU Browsers Tested
~40 kbps / Muted : Same, ~50 Packets, Muted : Same or slight drop ~30 kbps / Muted : Same, ~33 Packets, Muted : Same or slight drop ~70 kbps / Muted : Same, ~50 Packets, Muted : Same ~70 kbps / Muted : Same, ~55 Packets, Muted : Same Tested in Chrome, Not tested in Opera, Firefox, Safari, Edge

================================================ FILE: src/content/peerconnection/audio/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* global TimelineDataSeries, TimelineGraphView */ 'use strict'; const audio2 = document.querySelector('audio#audio2'); const callButton = document.querySelector('button#callButton'); const hangupButton = document.querySelector('button#hangupButton'); const codecSelector = document.querySelector('select#codec'); hangupButton.disabled = true; callButton.onclick = call; hangupButton.onclick = hangup; let pc1; let pc2; let localStream; let bitrateGraph; let bitrateSeries; let targetBitrateSeries; let headerrateSeries; let packetGraph; let packetSeries; let lastResult; const offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 0, voiceActivityDetection: false }; const audioLevels = []; let audioLevelGraph; let audioLevelSeries; // Enabling opus DTX is an expert option without GUI. // eslint-disable-next-line prefer-const let useDtx = false; // Disabling Opus FEC is an expert option without GUI. // eslint-disable-next-line prefer-const let useFec = true; // We only show one way of doing this. const codecPreferences = document.querySelector('#codecPreferences'); const supportsSetCodecPreferences = window.RTCRtpTransceiver && 'setCodecPreferences' in window.RTCRtpTransceiver.prototype; if (supportsSetCodecPreferences) { codecSelector.style.display = 'none'; const {codecs} = RTCRtpReceiver.getCapabilities('audio'); codecs.forEach(codec => { if (['audio/CN', 'audio/telephone-event'].includes(codec.mimeType)) { return; } const option = document.createElement('option'); option.value = (codec.mimeType + ' ' + codec.clockRate + ' ' + (codec.sdpFmtpLine || '')).trim(); option.innerText = option.value; codecPreferences.appendChild(option); }); codecPreferences.disabled = false; } else { codecPreferences.style.display = 'none'; } // Change the ptime. For opus supported values are [10, 20, 40, 60]. // Expert option without GUI. // eslint-disable-next-line no-unused-vars async function setPtime(ptime) { const offer = await pc1.createOffer(); await pc1.setLocalDescription(offer); const desc = pc1.remoteDescription; if (desc.sdp.indexOf('a=ptime:') !== -1) { desc.sdp = desc.sdp.replace(/a=ptime:.*/, 'a=ptime:' + ptime); } else { desc.sdp += 'a=ptime:' + ptime + '\r\n'; } await pc1.setRemoteDescription(desc); } function gotStream(stream) { hangupButton.disabled = false; console.log('Received local stream'); localStream = stream; const audioTracks = localStream.getAudioTracks(); if (audioTracks.length > 0) { console.log(`Using Audio device: ${audioTracks[0].label}`); } localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); console.log('Adding Local Stream to peer connection'); pc1.createOffer(offerOptions) .then(gotDescription1, onCreateSessionDescriptionError); bitrateSeries = new TimelineDataSeries(); bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas'); bitrateGraph.updateEndDate(); targetBitrateSeries = new TimelineDataSeries(); targetBitrateSeries.setColor('blue'); headerrateSeries = new TimelineDataSeries(); headerrateSeries.setColor('green'); packetSeries = new TimelineDataSeries(); packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas'); packetGraph.updateEndDate(); audioLevelSeries = new TimelineDataSeries(); audioLevelGraph = new TimelineGraphView('audioLevelGraph', 'audioLevelCanvas'); audioLevelGraph.updateEndDate(); } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); } function call() { callButton.disabled = true; codecSelector.disabled = true; console.log('Starting call'); const servers = null; pc1 = new RTCPeerConnection(servers); console.log('Created local peer connection object pc1'); pc1.onicecandidate = e => onIceCandidate(pc1, e); pc2 = new RTCPeerConnection(servers); console.log('Created remote peer connection object pc2'); pc2.onicecandidate = e => onIceCandidate(pc2, e); pc2.ontrack = gotRemoteStream; console.log('Requesting local stream'); navigator.mediaDevices .getUserMedia({ audio: true, video: false }) .then(gotStream) .catch(e => { alert(`getUserMedia() error: ${e.name}`); }); } function gotDescription1(desc) { console.log(`Offer from pc1\n${desc.sdp}`); pc1.setLocalDescription(desc) .then(() => { if (!supportsSetCodecPreferences) { desc.sdp = forceChosenAudioCodec(desc.sdp); } pc2.setRemoteDescription(desc).then(() => { return pc2.createAnswer().then(gotDescription2, onCreateSessionDescriptionError); }, onSetSessionDescriptionError); }, onSetSessionDescriptionError); } function gotDescription2(desc) { console.log(`Answer from pc2\n${desc.sdp}`); pc2.setLocalDescription(desc).then(() => { if (!supportsSetCodecPreferences) { desc.sdp = forceChosenAudioCodec(desc.sdp); } if (useDtx) { desc.sdp = desc.sdp.replace('useinbandfec=1', 'useinbandfec=1;usedtx=1'); } if (!useFec) { desc.sdp = desc.sdp.replace('useinbandfec=1', 'useinbandfec=0'); } pc1.setRemoteDescription(desc).then(() => {}, onSetSessionDescriptionError); }, onSetSessionDescriptionError); } function hangup() { console.log('Ending call'); localStream.getTracks().forEach(track => track.stop()); pc1.close(); pc2.close(); pc1 = null; pc2 = null; hangupButton.disabled = true; callButton.disabled = false; codecSelector.disabled = false; } function gotRemoteStream(e) { if (supportsSetCodecPreferences) { const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex]; if (preferredCodec.value !== '') { const [mimeType, clockRate, sdpFmtpLine] = preferredCodec.value.split(' '); const {codecs} = RTCRtpReceiver.getCapabilities('audio'); console.log(mimeType, clockRate, sdpFmtpLine); console.log(JSON.stringify(codecs, null, ' ')); const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.clockRate === parseInt(clockRate, 10) && c.sdpFmtpLine === sdpFmtpLine); const selectedCodec = codecs[selectedCodecIndex]; codecs.splice(selectedCodecIndex, 1); codecs.unshift(selectedCodec); e.transceiver.setCodecPreferences(codecs); console.log('Preferred video codec', selectedCodec); } } if (audio2.srcObject !== e.streams[0]) { audio2.srcObject = e.streams[0]; console.log('Received remote stream'); } } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function onIceCandidate(pc, event) { getOtherPc(pc).addIceCandidate(event.candidate) .then( () => onAddIceCandidateSuccess(pc), err => onAddIceCandidateError(pc, err) ); console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess() { console.log('AddIceCandidate success.'); } function onAddIceCandidateError(error) { console.log(`Failed to add ICE Candidate: ${error.toString()}`); } function onSetSessionDescriptionError(error) { console.log(`Failed to set session description: ${error.toString()}`); } function forceChosenAudioCodec(sdp) { return maybePreferCodec(sdp, 'audio', 'send', codecSelector.value); } // Copied from AppRTC's sdputils.js: // Sets |codec| as the default |type| codec if it's present. // The format of |codec| is 'NAME/RATE', e.g. 'opus/48000'. function maybePreferCodec(sdp, type, dir, codec) { const str = `${type} ${dir} codec`; if (codec === '') { console.log(`No preference on ${str}.`); return sdp; } console.log(`Prefer ${str}: ${codec}`); const sdpLines = sdp.split('\r\n'); // Search for m line. const mLineIndex = findLine(sdpLines, 'm=', type); if (mLineIndex === null) { return sdp; } // If the codec is available, set it as the default in m line. const codecIndex = findLine(sdpLines, 'a=rtpmap', codec); console.log('codecIndex', codecIndex); if (codecIndex) { const payload = getCodecPayloadType(sdpLines[codecIndex]); if (payload) { sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload); } } sdp = sdpLines.join('\r\n'); return sdp; } // Find the line in sdpLines that starts with |prefix|, and, if specified, // contains |substr| (case-insensitive search). function findLine(sdpLines, prefix, substr) { return findLineInRange(sdpLines, 0, -1, prefix, substr); } // Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix| // and, if specified, contains |substr| (case-insensitive search). function findLineInRange(sdpLines, startLine, endLine, prefix, substr) { const realEndLine = endLine !== -1 ? endLine : sdpLines.length; for (let i = startLine; i < realEndLine; ++i) { if (sdpLines[i].indexOf(prefix) === 0) { if (!substr || sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) { return i; } } } return null; } // Gets the codec payload type from an a=rtpmap:X line. function getCodecPayloadType(sdpLine) { const pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+'); const result = sdpLine.match(pattern); return (result && result.length === 2) ? result[1] : null; } // Returns a new m= line with the specified codec as the first one. function setDefaultCodec(mLine, payload) { const elements = mLine.split(' '); // Just copy the first three parameters; codec order starts on fourth. const newLine = elements.slice(0, 3); // Put target payload first and copy in the rest. newLine.push(payload); for (let i = 3; i < elements.length; i++) { if (elements[i] !== payload) { newLine.push(elements[i]); } } return newLine.join(' '); } // query getStats every second window.setInterval(() => { if (!pc1) { return; } const sender = pc1.getSenders()[0]; if (!sender) { return; } sender.getStats().then(res => { res.forEach(report => { let bytes; let headerBytes; let packets; if (report.type === 'outbound-rtp') { if (report.isRemote) { return; } const now = report.timestamp; bytes = report.bytesSent; headerBytes = report.headerBytesSent; packets = report.packetsSent; if (lastResult && lastResult.has(report.id)) { const deltaT = (now - lastResult.get(report.id).timestamp) / 1000; // calculate bitrate const bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent) / deltaT; const headerrate = 8 * (headerBytes - lastResult.get(report.id).headerBytesSent) / deltaT; // append to chart bitrateSeries.addPoint(now, bitrate); headerrateSeries.addPoint(now, headerrate); targetBitrateSeries.addPoint(now, report.targetBitrate); bitrateGraph.setDataSeries([bitrateSeries, headerrateSeries, targetBitrateSeries]); bitrateGraph.updateEndDate(); // calculate number of packets and append to chart packetSeries.addPoint(now, (packets - lastResult.get(report.id).packetsSent) / deltaT); packetGraph.setDataSeries([packetSeries]); packetGraph.updateEndDate(); } } }); lastResult = res; }); }, 1000); if (window.RTCRtpReceiver && ('getSynchronizationSources' in window.RTCRtpReceiver.prototype)) { let lastTime; const getAudioLevel = (timestamp) => { window.requestAnimationFrame(getAudioLevel); if (!pc2) { return; } const receiver = pc2.getReceivers().find(r => r.track.kind === 'audio'); if (!receiver) { return; } const sources = receiver.getSynchronizationSources(); sources.forEach(source => { audioLevels.push(source.audioLevel); }); if (!lastTime) { lastTime = timestamp; } else if (timestamp - lastTime > 500 && audioLevels.length > 0) { // Update graph every 500ms. const maxAudioLevel = Math.max.apply(null, audioLevels); audioLevelSeries.addPoint(Date.now(), maxAudioLevel); audioLevelGraph.setDataSeries([audioLevelSeries]); audioLevelGraph.updateEndDate(); audioLevels.length = 0; lastTime = timestamp; } }; window.requestAnimationFrame(getAudioLevel); } ================================================ FILE: src/content/peerconnection/audio/js/test.js ================================================ /* * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/peerconnection/audio/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('audio-only peerconnection', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('establishes a connection', async () => { await driver.findElement(webdriver.By.id('callButton')).click(); await driver.wait(() => driver.executeScript(() => { return pc1 && pc1.connectionState === 'connected'; // eslint-disable-line no-undef })); await driver.wait(() => driver.executeScript(() => { return pc2 && pc2.connectionState === 'connected'; // eslint-disable-line no-undef })); await driver.wait(() => driver.executeScript(() => { return document.getElementById('audio2').readyState === HTMLMediaElement.HAVE_ENOUGH_DATA; })); }); // TODO: assert usage of different codecs via getStats. }); ================================================ FILE: src/content/peerconnection/bandwidth/css/main.css ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); } button { margin: 0 20px 0 0; width: 96px; } video#localVideo { margin: 0 20px 20px 0; } div.label { display: inline-block; font-weight: 400; width: 120px; } div.graph-container { float: left; margin: 0.5em; width: calc(50% - 1em); } a#viewSource { clear: both; } ================================================ FILE: src/content/peerconnection/bandwidth/index.html ================================================ Peer connection: adjust bandwidth

WebRTC samples Peer connection: adjust bandwidth

kbps Use synthetic video:
Bitrate
Packets sent per second
View source on GitHub
================================================ FILE: src/content/peerconnection/bandwidth/js/main.js ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* global TimelineDataSeries, TimelineGraphView */ 'use strict'; const remoteVideo = document.querySelector('video#remoteVideo'); const localVideo = document.querySelector('video#localVideo'); const callButton = document.querySelector('button#callButton'); const hangupButton = document.querySelector('button#hangupButton'); const bandwidthSelector = document.querySelector('select#bandwidth'); const synthetic = document.querySelector('input#synthetic'); hangupButton.disabled = true; callButton.onclick = call; hangupButton.onclick = hangup; let pc1; let pc2; let localStream; // Can be set in the console before making a call to test this keeps // within the envelope set by the SDP. In kbps. // eslint-disable-next-line prefer-const let maxBandwidth = 0; let bitrateGraph; let bitrateSeries; let headerrateSeries; let packetGraph; let packetSeries; let lastResult; let lastRemoteStart = 0; // lastRemoteFullSizeDelay is designed to be picked up by a test script. // eslint-disable-next-line no-unused-vars let lastRemoteFullSizeDelay = 0; const offerOptions = { offerToReceiveAudio: 0, offerToReceiveVideo: 1 }; remoteVideo.addEventListener('resize', ev => { const elapsed = performance.now() - lastRemoteStart; console.log(elapsed, ': Resize event, size ', remoteVideo.videoWidth, 'x', remoteVideo.videoHeight); if (localVideo.videoWidth == remoteVideo.videoWidth && localVideo.videoHeight == remoteVideo.videoHeight) { lastRemoteFullSizeDelay = elapsed; console.log('Full size achieved'); } }); function gotStream(stream) { hangupButton.disabled = false; console.log('Received local stream'); localStream = stream; localVideo.srcObject = stream; localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); console.log('Adding Local Stream to peer connection'); pc1.createOffer( offerOptions ).then( gotDescription1, onCreateSessionDescriptionError ); bitrateSeries = new TimelineDataSeries(); bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas'); bitrateGraph.updateEndDate(); headerrateSeries = new TimelineDataSeries(); headerrateSeries.setColor('green'); packetSeries = new TimelineDataSeries(); packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas'); packetGraph.updateEndDate(); } function onCreateSessionDescriptionError(error) { console.log('Failed to create session description: ' + error.toString()); } function call() { callButton.disabled = true; bandwidthSelector.disabled = false; console.log('Starting call'); const servers = null; pc1 = new RTCPeerConnection(servers); console.log('Created local peer connection object pc1'); pc1.onicecandidate = onIceCandidate.bind(pc1); pc2 = new RTCPeerConnection(servers); console.log('Created remote peer connection object pc2'); pc2.onicecandidate = onIceCandidate.bind(pc2); pc2.ontrack = gotRemoteStream; if (synthetic.checked) { console.log('Requesting synthetic local stream'); gotStream(syntheticVideoStream()); } else { console.log('Requesting live local stream'); navigator.mediaDevices.getUserMedia({video: true}) .then(gotStream) .catch(e => alert('getUserMedia() error: ' + e.name)); } } function gotDescription1(desc) { console.log('Offer from pc1 \n' + desc.sdp); pc1.setLocalDescription(desc).then( () => { pc2.setRemoteDescription(desc) .then(() => pc2.createAnswer().then(gotDescription2, onCreateSessionDescriptionError), onSetSessionDescriptionError); }, onSetSessionDescriptionError ); } function gotDescription2(desc) { pc2.setLocalDescription(desc).then( () => { console.log('Answer from pc2 \n' + desc.sdp); let p; if (maxBandwidth) { p = pc1.setRemoteDescription({ type: desc.type, sdp: updateBandwidthRestriction(desc.sdp, maxBandwidth) }); } else { p = pc1.setRemoteDescription(desc); } p.then(() => {}, onSetSessionDescriptionError); }, onSetSessionDescriptionError ); } function hangup() { console.log('Ending call'); localStream.getTracks().forEach(track => track.stop()); pc1.close(); pc2.close(); pc1 = null; pc2 = null; hangupButton.disabled = true; callButton.disabled = false; bandwidthSelector.disabled = true; } function gotRemoteStream(e) { if (remoteVideo.srcObject !== e.streams[0]) { remoteVideo.srcObject = e.streams[0]; console.log('Received remote stream'); lastRemoteStart = performance.now(); lastRemoteFullSizeDelay = 0; } } function getOtherPc(pc) { return pc === pc1 ? pc2 : pc1; } function getName(pc) { return pc === pc1 ? 'pc1' : 'pc2'; } function onIceCandidate(event) { getOtherPc(this) .addIceCandidate(event.candidate) .then(onAddIceCandidateSuccess) .catch(onAddIceCandidateError); console.log(`${getName(this)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess() { console.log('AddIceCandidate success.'); } function onAddIceCandidateError(error) { console.log('Failed to add ICE Candidate: ' + error.toString()); } function onSetSessionDescriptionError(error) { console.log('Failed to set session description: ' + error.toString()); } // renegotiate bandwidth on the fly. bandwidthSelector.onchange = () => { bandwidthSelector.disabled = true; const bandwidth = bandwidthSelector.options[bandwidthSelector.selectedIndex].value; setBandwidth(bandwidth) .then(() => { bandwidthSelector.disabled = false; }); }; function setBandwidth(bandwidthInKbps) { // In modern browsers, use RTCRtpSender.setParameters to change bandwidth without // (local) renegotiation. Note that this will be within the envelope of // the initial maximum bandwidth negotiated via SDP. if ((adapter.browserDetails.browser === 'chrome' || adapter.browserDetails.browser === 'safari' || (adapter.browserDetails.browser === 'firefox' && adapter.browserDetails.version >= 64)) && 'RTCRtpSender' in window && 'setParameters' in window.RTCRtpSender.prototype) { const sender = pc1.getSenders()[0]; const parameters = sender.getParameters(); if (!parameters.encodings) { parameters.encodings = [{}]; } if (bandwidthInKbps === 'unlimited') { delete parameters.encodings[0].maxBitrate; } else { parameters.encodings[0].maxBitrate = bandwidthInKbps * 1000; } return sender.setParameters(parameters); } // Fallback to the SDP changes with local renegotiation as way of limiting // the bandwidth. return pc1.createOffer() .then(offer => pc1.setLocalDescription(offer)) .then(() => { const desc = { type: pc1.remoteDescription.type, sdp: bandwidthInKbps === 'unlimited' ? removeBandwidthRestriction(pc1.remoteDescription.sdp) : updateBandwidthRestriction(pc1.remoteDescription.sdp, bandwidthInKbps) }; console.log('Applying bandwidth restriction to setRemoteDescription:\n' + desc.sdp); return pc1.setRemoteDescription(desc); }) .catch(onSetSessionDescriptionError); }; function updateBandwidthRestriction(sdp, bandwidth) { let modifier = 'AS'; if (adapter.browserDetails.browser === 'firefox') { bandwidth = (bandwidth >>> 0) * 1000; modifier = 'TIAS'; } if (sdp.indexOf('b=' + modifier + ':') === -1) { // insert b= after c= line. sdp = sdp.replace(/c=IN (.*)\r\n/, 'c=IN $1\r\nb=' + modifier + ':' + bandwidth + '\r\n'); } else { sdp = sdp.replace(new RegExp('b=' + modifier + ':.*\r\n'), 'b=' + modifier + ':' + bandwidth + '\r\n'); } return sdp; } function removeBandwidthRestriction(sdp) { return sdp.replace(/b=AS:.*\r\n/, '').replace(/b=TIAS:.*\r\n/, ''); } // query getStats every second window.setInterval(() => { if (!pc1) { return; } const sender = pc1.getSenders()[0]; if (!sender) { return; } sender.getStats().then(res => { res.forEach(report => { let bytes; let headerBytes; let packets; if (report.type === 'outbound-rtp') { if (report.isRemote) { return; } const now = report.timestamp; bytes = report.bytesSent; headerBytes = report.headerBytesSent; packets = report.packetsSent; if (lastResult && lastResult.has(report.id)) { // calculate bitrate const bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent) / (now - lastResult.get(report.id).timestamp); const headerrate = 8 * (headerBytes - lastResult.get(report.id).headerBytesSent) / (now - lastResult.get(report.id).timestamp); // append to chart bitrateSeries.addPoint(now, bitrate); headerrateSeries.addPoint(now, headerrate); bitrateGraph.setDataSeries([bitrateSeries, headerrateSeries]); bitrateGraph.updateEndDate(); // calculate number of packets and append to chart packetSeries.addPoint(now, packets - lastResult.get(report.id).packetsSent); packetGraph.setDataSeries([packetSeries]); packetGraph.updateEndDate(); } } }); lastResult = res; }); }, 1000); // Return a number between 0 and maxValue based on the input number, // so that the output changes smoothly up and down. function triangle(number, maxValue) { const modulus = (maxValue + 1) * 2; return Math.abs(number % modulus - maxValue); } function syntheticVideoStream({width = 640, height = 480, signal} = {}) { const canvas = Object.assign( document.createElement('canvas'), {width, height} ); const ctx = canvas.getContext('2d'); const stream = canvas.captureStream(); let count = 0; setInterval(() => { // Use relatively-prime multipliers to get a color roll const r = triangle(count*2, 255); const g = triangle(count*3, 255); const b = triangle(count*5, 255); ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; count += 1; const boxSize=80; ctx.fillRect(0, 0, width, height); // Add some bouncing boxes in contrast color to add a little more noise. const rContrast = (r + 128)%256; const gContrast = (g + 128)%256; const bContrast = (b + 128)%256; ctx.fillStyle = `rgb(${rContrast}, ${gContrast}, ${bContrast})`; const xpos = triangle(count*5, width - boxSize); const ypos = triangle(count*7, height - boxSize); ctx.fillRect(xpos, ypos, boxSize, boxSize); const xpos2 = triangle(count*11, width - boxSize); const ypos2 = triangle(count*13, height - boxSize); ctx.fillRect(xpos2, ypos2, boxSize, boxSize); // If signal is set (0-255), add a constant-color box of that luminance to // the video frame at coordinates 20 to 60 in both X and Y direction. // (big enough to avoid color bleed from surrounding video in some codecs, // for more stable tests). if (signal != undefined) { ctx.fillStyle = `rgb(${signal}, ${signal}, ${signal})`; ctx.fillRect(20, 20, 40, 40); } }, 100); return stream; } ================================================ FILE: src/content/peerconnection/change-codecs/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 0 0; width: 83px; } button#hangupButton { margin: 0; } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); margin: 0 0 20px 0; vertical-align: top; } video#localVideo { margin: 0 20px 20px 0; } div.box { margin: 1em; } @media screen and (max-width: 400px) { button { width: 83px; margin: 0 11px 10px 0; } video { height: 90px; margin: 0 0 10px 0; width: calc(50% - 7px); } video#localVideo { margin: 0 10px 20px 0; } } ================================================ FILE: src/content/peerconnection/change-codecs/index.html ================================================ Peer connection

WebRTC samples Peer connection

This sample shows how to setup a connection between two peers using RTCPeerConnection and choose the preferred video codec to use (when that functionality is available.)

Codec preferences:

View the console to see logging. The MediaStream object localStream, and the RTCPeerConnection objects pc1 and pc2 are in global scope, so you can inspect them in the console as well.

For more information about RTCPeerConnection, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/peerconnection/change-codecs/js/main.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); const hangupButton = document.getElementById('hangupButton'); callButton.disabled = true; hangupButton.disabled = true; startButton.addEventListener('click', start); callButton.addEventListener('click', call); hangupButton.addEventListener('click', hangup); let startTime; const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); localVideo.addEventListener('loadedmetadata', function() { console.log(`Local video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.addEventListener('loadedmetadata', function() { console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.addEventListener('resize', () => { console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`); // We'll use the first onsize callback as an indication that video has started // playing out. if (startTime) { const elapsedTime = window.performance.now() - startTime; console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms'); startTime = null; } }); const codecPreferences = document.getElementById('codecPreferences'); const supportsSetCodecPreferences = window.RTCRtpTransceiver && 'setCodecPreferences' in window.RTCRtpTransceiver.prototype; let localStream; let pc1; let pc2; function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } async function start() { console.log('Requesting local stream'); startButton.disabled = true; try { const stream = await navigator.mediaDevices.getUserMedia({video: true}); console.log('Received local stream'); localVideo.srcObject = stream; localStream = stream; callButton.disabled = false; } catch (e) { alert(`getUserMedia() error: ${e.name}`); } if (supportsSetCodecPreferences) { const {codecs} = RTCRtpReceiver.getCapabilities('video'); codecs.forEach(codec => { if (['video/red', 'video/ulpfec', 'video/rtx', 'video/flexfec-03'].includes(codec.mimeType)) { return; } const option = document.createElement('option'); option.value = (codec.mimeType + ' ' + (codec.sdpFmtpLine || '')).trim(); option.innerText = option.value; codecPreferences.appendChild(option); }); codecPreferences.disabled = false; } } async function call() { callButton.disabled = true; hangupButton.disabled = false; console.log('Starting call'); startTime = window.performance.now(); const videoTracks = localStream.getVideoTracks(); if (videoTracks.length > 0) { console.log(`Using video device: ${videoTracks[0].label}`); } const configuration = {}; console.log('RTCPeerConnection configuration:', configuration); pc1 = new RTCPeerConnection(configuration); console.log('Created local peer connection object pc1'); pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e)); pc2 = new RTCPeerConnection(configuration); console.log('Created remote peer connection object pc2'); pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e)); pc2.addEventListener('track', gotRemoteStream); localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); console.log('Added local stream to pc1'); codecPreferences.disabled = true; try { console.log('pc1 createOffer start'); const offer = await pc1.createOffer(); await onCreateOfferSuccess(offer); } catch (e) { onCreateSessionDescriptionError(e); } } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); } async function onCreateOfferSuccess(desc) { console.log(`Offer from pc1\n${desc.sdp}`); console.log('pc1 setLocalDescription start'); try { await pc1.setLocalDescription(desc); onSetLocalSuccess(pc1); } catch (e) { onSetSessionDescriptionError(); } console.log('pc2 setRemoteDescription start'); try { await pc2.setRemoteDescription(desc); onSetRemoteSuccess(pc2); } catch (e) { onSetSessionDescriptionError(); } console.log('pc2 createAnswer start'); try { const answer = await pc2.createAnswer(); await onCreateAnswerSuccess(answer); } catch (e) { onCreateSessionDescriptionError(e); } } function onSetLocalSuccess(pc) { console.log(`${getName(pc)} setLocalDescription complete`); } function onSetRemoteSuccess(pc) { console.log(`${getName(pc)} setRemoteDescription complete`); } function onSetSessionDescriptionError(error) { console.log(`Failed to set session description: ${error.toString()}`); } function gotRemoteStream(e) { // Set codec preferences on the receiving side. if (e.track.kind === 'video' && supportsSetCodecPreferences) { const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex]; if (preferredCodec.value !== '') { const [mimeType, sdpFmtpLine] = preferredCodec.value.split(' '); const {codecs} = RTCRtpReceiver.getCapabilities('video'); const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.sdpFmtpLine === sdpFmtpLine); const selectedCodec = codecs[selectedCodecIndex]; codecs.splice(selectedCodecIndex, 1); codecs.unshift(selectedCodec); e.transceiver.setCodecPreferences(codecs); console.log('Receiver\'s preferred video codec', selectedCodec); } } if (remoteVideo.srcObject !== e.streams[0]) { remoteVideo.srcObject = e.streams[0]; console.log('pc2 received remote stream'); } } async function onCreateAnswerSuccess(desc) { console.log(`Answer from pc2:\n${desc.sdp}`); console.log('pc2 setLocalDescription start'); try { await pc2.setLocalDescription(desc); onSetLocalSuccess(pc2); } catch (e) { onSetSessionDescriptionError(e); } console.log('pc1 setRemoteDescription start'); try { await pc1.setRemoteDescription(desc); onSetRemoteSuccess(pc1); // Display the video codec that is actually used. setTimeout(async () => { const stats = await pc1.getStats(); stats.forEach(stat => { if (!(stat.type === 'outbound-rtp' && stat.kind === 'video')) { return; } const codec = stats.get(stat.codecId); document.getElementById('actualCodec').innerText = 'Using ' + codec.mimeType + (codec.sdpFmtpLine ? ' ' + codec.sdpFmtpLine + ' ' : '') + ', payloadType=' + codec.payloadType + '.'; if (stat.encoderImplementation) { document.getElementById('actualCodec').innerText += ' Encoder: "' + stat.encoderImplementation + '".'; } if (stat.powerEfficientEncoder !== undefined) { document.getElementById('actualCodec').innerText += ' Power efficient: ' + stat.powerEfficientEncoder + '.'; } }); }, 1000); } catch (e) { onSetSessionDescriptionError(e); } } async function onIceCandidate(pc, event) { try { await (getOtherPc(pc).addIceCandidate(event.candidate)); onAddIceCandidateSuccess(pc); } catch (e) { onAddIceCandidateError(pc, e); } console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess(pc) { console.log(`${getName(pc)} addIceCandidate success`); } function onAddIceCandidateError(pc, error) { console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`); } function hangup() { console.log('Ending call'); pc1.close(); pc2.close(); pc1 = null; pc2 = null; hangupButton.disabled = true; callButton.disabled = false; codecPreferences.disabled = false; } ================================================ FILE: src/content/peerconnection/change-codecs/js/test.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/peerconnection/change-codecs/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('peerconnection with setCodecPreferences', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); ['video/VP8'].forEach(codec => { it('establishes a connection', async () => { await driver.findElement(webdriver.By.id('startButton')).click(); await driver.wait(() => driver.executeScript(() => { return localStream !== null; // eslint-disable-line no-undef })); await driver.wait(() => driver.executeScript(() => { return codecPreferences.disabled === false; // eslint-disable-line no-undef })); await driver.findElement(webdriver.By.id('codecPreferences')).click(); await driver.findElement(webdriver.By.css('option[value=\'video/VP8\']')).click(); await driver.wait(() => driver.findElement(webdriver.By.id('callButton')).isEnabled()); await driver.findElement(webdriver.By.id('callButton')).click(); await Promise.all([ await driver.wait(() => driver.executeScript(() => { return pc1 && pc1.connectionState === 'connected'; // eslint-disable-line no-undef })), await driver.wait(() => driver.executeScript(() => { return pc2 && pc2.connectionState === 'connected'; // eslint-disable-line no-undef })), ]); await driver.wait(() => driver.executeScript(() => { return document.getElementById('remoteVideo').readyState === HTMLMediaElement.HAVE_ENOUGH_DATA; })); await driver.wait(() => driver.executeScript(() => { return document.getElementById('actualCodec').innerText !== ''; })); const actualCodec = await driver.findElement(webdriver.By.id('actualCodec')).getAttribute('innerText'); expect(actualCodec.startsWith('Using ' + codec)).toBe(true); }); }); }); ================================================ FILE: src/content/peerconnection/channel/css/main.css ================================================ /* * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 0 0; width: 83px; } button#hangupButton { margin: 0; } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); margin: 0 0 20px 0; vertical-align: top; } video#localVideo { margin: 0 20px 20px 0; } div.box { margin: 1em; } @media screen and (max-width: 400px) { button { width: 83px; margin: 0 11px 10px 0; } video { height: 90px; margin: 0 0 10px 0; width: calc(50% - 7px); } video#localVideo { margin: 0 10px 20px 0; } } ================================================ FILE: src/content/peerconnection/channel/index.html ================================================ Peer connection between two tabs

WebRTC samples Peer connection

This sample shows how to setup a connection between two peers in different tabs using RTCPeerConnection and Broadcast Channel

Click the start button in two tabs (of the same browser; can be in different windows) to make a call

View source on GitHub
================================================ FILE: src/content/peerconnection/channel/js/main.js ================================================ /* * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const startButton = document.getElementById('startButton'); const hangupButton = document.getElementById('hangupButton'); hangupButton.disabled = true; const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); let pc; let localStream; const signaling = new BroadcastChannel('webrtc'); signaling.onmessage = e => { if (!localStream) { console.log('not ready yet'); return; } switch (e.data.type) { case 'offer': handleOffer(e.data); break; case 'answer': handleAnswer(e.data); break; case 'candidate': handleCandidate(e.data); break; case 'ready': // A second tab joined. This tab will initiate a call unless in a call already. if (pc) { console.log('already in call, ignoring'); return; } makeCall(); break; case 'bye': if (pc) { hangup(); } break; default: console.log('unhandled', e); break; } }; startButton.onclick = async () => { localStream = await navigator.mediaDevices.getUserMedia({audio: true, video: true}); localVideo.srcObject = localStream; startButton.disabled = true; hangupButton.disabled = false; signaling.postMessage({type: 'ready'}); }; hangupButton.onclick = async () => { hangup(); signaling.postMessage({type: 'bye'}); }; async function hangup() { if (pc) { pc.close(); pc = null; } localStream.getTracks().forEach(track => track.stop()); localStream = null; startButton.disabled = false; hangupButton.disabled = true; }; function createPeerConnection() { pc = new RTCPeerConnection(); pc.onicecandidate = e => { const message = { type: 'candidate', candidate: null, }; if (e.candidate) { message.candidate = e.candidate.candidate; message.sdpMid = e.candidate.sdpMid; message.sdpMLineIndex = e.candidate.sdpMLineIndex; } signaling.postMessage(message); }; pc.ontrack = e => remoteVideo.srcObject = e.streams[0]; localStream.getTracks().forEach(track => pc.addTrack(track, localStream)); } async function makeCall() { await createPeerConnection(); const offer = await pc.createOffer(); signaling.postMessage({type: 'offer', sdp: offer.sdp}); await pc.setLocalDescription(offer); } async function handleOffer(offer) { if (pc) { console.error('existing peerconnection'); return; } await createPeerConnection(); await pc.setRemoteDescription(offer); const answer = await pc.createAnswer(); signaling.postMessage({type: 'answer', sdp: answer.sdp}); await pc.setLocalDescription(answer); } async function handleAnswer(answer) { if (!pc) { console.error('no peerconnection'); return; } await pc.setRemoteDescription(answer); } async function handleCandidate(candidate) { if (!pc) { console.error('no peerconnection'); return; } if (!candidate.candidate) { await pc.addIceCandidate(null); } else { await pc.addIceCandidate(candidate); } } ================================================ FILE: src/content/peerconnection/channel/js/test.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/peerconnection/channel/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('peerconnection and broadcast channels', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(async () => { await driver.get(url); }); it('establishes a connection and hangs up', async () => { const firstTab = await driver.getWindowHandle(); await driver.switchTo().window(firstTab); await driver.findElement(webdriver.By.id('startButton')).click(); await driver.wait(() => driver.executeScript(() => { return localStream !== null; // eslint-disable-line no-undef })); // Create a second tab, switch to it. await driver.switchTo().newWindow('tab'); const secondTab = await driver.getWindowHandle(); await driver.get(url); await driver.switchTo().window(secondTab); await driver.findElement(webdriver.By.id('startButton')).click(); await driver.wait(() => driver.executeScript(() => { return localStream !== null; // eslint-disable-line no-undef })); // Assert state in first tab. await driver.switchTo().window(firstTab); await driver.wait(() => driver.executeScript(() => { return pc && pc.connectionState === 'connected'; // eslint-disable-line no-undef })); await driver.wait(() => driver.executeScript(() => { return document.getElementById('remoteVideo').readyState === 4; })); // Assert state in second tab. await driver.switchTo().window(secondTab); await driver.wait(() => driver.executeScript(() => { return pc && pc.connectionState === 'connected'; // eslint-disable-line no-undef })); await driver.wait(() => driver.executeScript(() => { return document.getElementById('remoteVideo').readyState === 4; })); }); }); ================================================ FILE: src/content/peerconnection/constraints/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 25px 0; vertical-align: top; width: 134px; } div#getUserMedia { padding: 0 0 8px 0; } div.input { display: inline-block; margin: 0 4px 0 0; vertical-align: top; width: 310px; } div.input > div { margin: 0 0 20px 0; vertical-align: top; } div.output { background-color: #eee; display: inline-block; font-family: 'Inconsolata', 'Courier New', monospace; font-size: 0.9em; padding: 10px 10px 10px 25px; position: relative; top: 10px; white-space: pre; width: 270px; } section#statistics div { display: inline-block; font-family: 'Inconsolata', 'Courier New', monospace; vertical-align: top; width: 308px; } section#statistics div#senderStats { margin: 0 20px 0 0; } section#constraints > div { margin: 0 0 20px 0; } section#video > div { display: inline-block; margin: 0 20px 0 0; vertical-align: top; width: calc(50% - 22px); } section#video > div div { font-size: 0.9em; margin: 0 0 0.5em 0; width: 320px; } h2 { margin: 0 0 1em 0; } section#constraints label { display: inline-block; width: 156px; } section { margin: 0 0 20px 0; padding: 0 0 15px 0; } section#video { width: calc(100% + 20px); } video { --width: 90%; width: var(--width); height: calc(var(--width) * 0.75); margin: 0 0 10px 0; } @media screen and (max-width: 720px) { button { font-weight: 500; height: 56px; line-height: 1.3em; width: 90px; } div#getUserMedia { padding: 0 0 40px 0; } section#statistics div { width: calc(50% - 14px); } video { height: 96px; } } ================================================ FILE: src/content/peerconnection/constraints/index.html ================================================ Constraints and statistics

WebRTC samples Constraints & statistics

This demo shows ways to use constraints and statistics in WebRTC applications.

Set camera constraints, and click Get media to (re)open the camera with these included. Click Connect to create a (local) peer connection. The RTCPeerConnection objects pc1 and pc2 can be inspected from the console.

Setting a value to zero will remove that constraint.

The lefthand video shows the output of getUserMedia(); on the right is the video after being passed through the peer connection. The transmission bitrate is displayed below the righthand video.

Camera constraints


View source on GitHub
================================================ FILE: src/content/peerconnection/constraints/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const getMediaButton = document.querySelector('button#getMedia'); const connectButton = document.querySelector('button#connect'); const hangupButton = document.querySelector('button#hangup'); getMediaButton.onclick = getMedia; connectButton.onclick = createPeerConnection; hangupButton.onclick = hangup; const minWidthInput = document.querySelector('div#minWidth input'); const maxWidthInput = document.querySelector('div#maxWidth input'); const minHeightInput = document.querySelector('div#minHeight input'); const maxHeightInput = document.querySelector('div#maxHeight input'); const minFramerateInput = document.querySelector('div#minFramerate input'); const maxFramerateInput = document.querySelector('div#maxFramerate input'); minWidthInput.onchange = maxWidthInput.onchange = minHeightInput.onchange = maxHeightInput.onchange = minFramerateInput.onchange = maxFramerateInput.onchange = displayRangeValue; const getUserMediaConstraintsDiv = document.querySelector('div#getUserMediaConstraints'); const bitrateDiv = document.querySelector('div#bitrate'); const peerDiv = document.querySelector('div#peer'); const senderStatsDiv = document.querySelector('div#senderStats'); const receiverStatsDiv = document.querySelector('div#receiverStats'); const localVideo = document.querySelector('div#localVideo video'); const remoteVideo = document.querySelector('div#remoteVideo video'); const localVideoStatsDiv = document.querySelector('div#localVideo div'); const remoteVideoStatsDiv = document.querySelector('div#remoteVideo div'); const updateStats = document.querySelector('input#updateStats'); let pc1; let pc2; let localStream; let bytesPrev; let timestampPrev; main(); function main() { displayGetUserMediaConstraints(); } function hangup() { console.log('Ending call'); pc1.close(); pc2.close(); // query stats one last time. Promise .all([ pc2 .getStats() .then(showRemoteStats, err => console.log(err)), pc1 .getStats() .then(showLocalStats, err => console.log(err)) ]) .then(() => { pc1 = null; pc2 = null; }); localStream.getTracks().forEach(track => track.stop()); localStream = null; hangupButton.disabled = true; getMediaButton.disabled = false; } function getMedia() { getMediaButton.disabled = true; if (localStream) { localStream.getTracks().forEach(track => track.stop()); const videoTracks = localStream.getVideoTracks(); for (let i = 0; i !== videoTracks.length; ++i) { videoTracks[i].stop(); } } navigator.mediaDevices.getUserMedia(getUserMediaConstraints()) .then(gotStream) .catch(e => { const message = `getUserMedia error: ${e.name}\nThis may mean invalid constraints.`; alert(message); console.log(message); getMediaButton.disabled = false; }); } function gotStream(stream) { connectButton.disabled = false; console.log('GetUserMedia succeeded'); localStream = stream; localVideo.srcObject = stream; } function getUserMediaConstraints() { const constraints = {}; constraints.audio = true; constraints.video = {}; if (minWidthInput.value !== '0') { constraints.video.width = {}; constraints.video.width.min = minWidthInput.value; } if (maxWidthInput.value !== '0') { constraints.video.width = constraints.video.width || {}; constraints.video.width.max = maxWidthInput.value; } if (minHeightInput.value !== '0') { constraints.video.height = {}; constraints.video.height.min = minHeightInput.value; } if (maxHeightInput.value !== '0') { constraints.video.height = constraints.video.height || {}; constraints.video.height.max = maxHeightInput.value; } if (minFramerateInput.value !== '0') { constraints.video.frameRate = {}; constraints.video.frameRate.min = minFramerateInput.value; } if (maxFramerateInput.value !== '0') { constraints.video.frameRate = constraints.video.frameRate || {}; constraints.video.frameRate.max = maxFramerateInput.value; } return constraints; } function displayGetUserMediaConstraints() { const constraints = getUserMediaConstraints(); console.log('getUserMedia constraints', constraints); getUserMediaConstraintsDiv.textContent = JSON.stringify(constraints, null, ' '); } function createPeerConnection() { connectButton.disabled = true; hangupButton.disabled = false; bytesPrev = 0; timestampPrev = 0; pc1 = new RTCPeerConnection(null); pc2 = new RTCPeerConnection(null); localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); console.log('pc1 creating offer'); pc1.onnegotiationneeded = () => console.log('Negotiation needed - pc1'); pc2.onnegotiationneeded = () => console.log('Negotiation needed - pc2'); pc1.onicecandidate = e => { console.log('Candidate pc1'); pc2 .addIceCandidate(e.candidate) .then(onAddIceCandidateSuccess, onAddIceCandidateError); }; pc2.onicecandidate = e => { console.log('Candidate pc2'); pc1 .addIceCandidate(e.candidate) .then(onAddIceCandidateSuccess, onAddIceCandidateError); }; pc2.ontrack = e => { if (remoteVideo.srcObject !== e.streams[0]) { console.log('pc2 got stream'); remoteVideo.srcObject = e.streams[0]; } }; pc1.createOffer().then( desc => { console.log('pc1 offering'); pc1.setLocalDescription(desc); pc2.setRemoteDescription(desc); pc2.createAnswer().then( desc2 => { console.log('pc2 answering'); pc2.setLocalDescription(desc2); pc1.setRemoteDescription(desc2); }, err => console.log(err) ); }, err => console.log(err) ); } function onAddIceCandidateSuccess() { console.log('AddIceCandidate success.'); } function onAddIceCandidateError(error) { console.log(`Failed to add Ice Candidate: ${error.toString()}`); } function showRemoteStats(results) { const statsString = dumpStats(results); receiverStatsDiv.innerHTML = `

Receiver stats

${statsString}`; // calculate video bitrate results.forEach(report => { const now = report.timestamp; let bitrate; if (report.type === 'inbound-rtp' && report.mediaType === 'video') { const bytes = report.bytesReceived; if (timestampPrev) { bitrate = 8 * (bytes - bytesPrev) / (now - timestampPrev); bitrate = Math.floor(bitrate); } bytesPrev = bytes; timestampPrev = now; } if (bitrate) { bitrate += ' kbits/sec'; bitrateDiv.innerHTML = `Bitrate:${bitrate}`; } }); // figure out the peer's ip let activeCandidatePair = null; let remoteCandidate = null; // Search for the candidate pair, spec-way first. results.forEach(report => { if (report.type === 'transport') { activeCandidatePair = results.get(report.selectedCandidatePairId); } }); // Fallback for Firefox. if (!activeCandidatePair) { results.forEach(report => { if (report.type === 'candidate-pair' && report.selected) { activeCandidatePair = report; } }); } if (activeCandidatePair && activeCandidatePair.remoteCandidateId) { remoteCandidate = results.get(activeCandidatePair.remoteCandidateId); } if (remoteCandidate) { if (remoteCandidate.address && remoteCandidate.port) { peerDiv.innerHTML = `Connected to:${remoteCandidate.address}:${remoteCandidate.port}`; } else if (remoteCandidate.ip && remoteCandidate.port) { peerDiv.innerHTML = `Connected to:${remoteCandidate.ip}:${remoteCandidate.port}`; } else if (remoteCandidate.ipAddress && remoteCandidate.portNumber) { // Fall back to old names. peerDiv.innerHTML = `Connected to:${remoteCandidate.ipAddress}:${remoteCandidate.portNumber}`; } } } function showLocalStats(results) { const statsString = dumpStats(results); senderStatsDiv.innerHTML = `

Sender stats

${statsString}`; } // Display statistics setInterval(() => { if (!updateStats.checked) { return; } if (pc1 && pc2) { pc2 .getStats() .then(showRemoteStats, err => console.log(err)); pc1 .getStats() .then(showLocalStats, err => console.log(err)); } else { console.log('Not connected yet'); } // Collect some stats from the video tags. if (localVideo.videoWidth) { const width = localVideo.videoWidth; const height = localVideo.videoHeight; localVideoStatsDiv.innerHTML = `Video dimensions: ${width}x${height}px`; } if (remoteVideo.videoWidth) { const rHeight = remoteVideo.videoHeight; const rWidth = remoteVideo.videoWidth; remoteVideoStatsDiv.innerHTML = `Video dimensions: ${rWidth}x${rHeight}px`; } }, 1000); // Dumping a stats variable as a string. // might be named toString? function dumpStats(results) { let statsString = ''; results.forEach(report => { statsString += '

Report type='; statsString += report.type; statsString += '

\n'; statsString += `id: ${report.id}
`; statsString += `timestamp: ${report.timestamp}
`; Object.keys(report).forEach(key => { if (['id', 'timestamp', 'type'].includes(key)) return; if (typeof report[key] === 'object') { statsString += `${key}: ${JSON.stringify(report[key])}
`; } else { statsString += `${key}: ${report[key]}
`; } }); }); return statsString; } // Utility to show the value of a range in a sibling span element function displayRangeValue(e) { const span = e.target.parentElement.querySelector('span'); span.textContent = e.target.value; displayGetUserMediaConstraints(); } ================================================ FILE: src/content/peerconnection/create-offer/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 20px 10px 0 0; width: 100px; } div#constraints { margin: 0 0 20px 0; } div#numAudioTracks { margin: 0 0 20px 0; } div#constraints div { margin: 0 0 10px 0; } div#constraints input { margin: 0 10px 0 0; position: relative; top: -2px; } div#numAudioTracks input { max-width: 30%; position: relative; top: 2px; width: 200px; } label { font-weight: 500; margin: 0 10px 0 0; } textarea { height: 200px; width: 100%; } ================================================ FILE: src/content/peerconnection/create-offer/index.html ================================================ createOffer() output

WebRTC samples createOffer() output

This page tests the createOffer() method. It creates a peer connection, then prints out the SDP generated by createOffer(), with the number of desired audio MediaStreamTracks and the checked constraints. Currently, only audio tracks can be added, as there is no programmatic way to generate video tracks. (Web Audio is used to generate the audio tracks.)

1
View source on GitHub
================================================ FILE: src/content/peerconnection/create-offer/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const audioInput = document.querySelector('input#audio'); const restartInput = document.querySelector('input#restart'); const vadInput = document.querySelector('input#vad'); const videoInput = document.querySelector('input#video'); const numAudioTracksInput = document.querySelector('div#numAudioTracks input'); const numAudioTracksDisplay = document.querySelector('span#numAudioTracksDisplay'); const outputTextarea = document.querySelector('textarea#output'); const createOfferButton = document.querySelector('button#createOffer'); createOfferButton.addEventListener('click', createOffer); numAudioTracksInput.addEventListener('change', e => numAudioTracksDisplay.innerText = e.target.value); async function createOffer() { outputTextarea.value = ''; const peerConnection = window.peerConnection = new RTCPeerConnection(null); const numRequestedAudioTracks = parseInt(numAudioTracksInput.value); for (let i = 0; i < numRequestedAudioTracks; i++) { const acx = new AudioContext(); const dst = acx.createMediaStreamDestination(); // Fill up the peer connection with numRequestedAudioTracks number of tracks. const track = dst.stream.getTracks()[0]; peerConnection.addTrack(track, dst.stream); } const offerOptions = { // New spec states offerToReceiveAudio/Video are of type long (due to // having to tell how many "m" lines to generate). // http://w3c.github.io/webrtc-pc/#idl-def-RTCOfferAnswerOptions. offerToReceiveAudio: (audioInput.checked) ? 1 : 0, offerToReceiveVideo: (videoInput.checked) ? 1 : 0, iceRestart: restartInput.checked, voiceActivityDetection: vadInput.checked }; try { const offer = await peerConnection.createOffer(offerOptions); await peerConnection.setLocalDescription(offer); outputTextarea.value = offer.sdp; } catch (e) { outputTextarea.value = `Failed to create offer: ${e}`; } } ================================================ FILE: src/content/peerconnection/dtmf/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 20px 0; width: 96px; } div#dialPad button { background-color: #ddd; border: 1px solid #ccc; color: black; font-size: 1em; font-weight: 400; height: 40px; margin: 0 10px 10px 0; width: 40px; } div#dialPad button:hover { background-color: #aaa; } div#dialPad button:active { background-color: #888; } div#dialPad { display: inline-block; margin: 0 20px 20px 0; vertical-align: top; } div#parameters { margin: 0 0 25px 0; } div#parameters > div { height: 28px; margin: 0 0 10px 0; } div#dtmf { background-color: #eee; display: inline-block; height: 180px; margin: 0 0 20px 0; padding: 5px 5px 5px 10px; width: calc(100% - 239px); } div#dtmf div { font-family: 'Inconsolata', 'Courier New', monospace; } div#sentTones { display: inline-block; line-height: 1.2em; } div#dtmfStatus { margin: 0 0 10px 0; } div#parameters input[type = range] { font-size: 1em; width: 85px; } div#parameters input#tones { width: calc(100% - 78px); } div#parameters label { display: inline-block; font-weight: 400; height: 28px; position: relative; top: 4px; vertical-align: top; width: 68px; } ================================================ FILE: src/content/peerconnection/dtmf/index.html ================================================ DTMF

WebRTC samples Send DTMF tones

Sent tones

500
50
View source on GitHub
================================================ FILE: src/content/peerconnection/dtmf/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const callButton = document.querySelector('button#callButton'); const sendTonesButton = document.querySelector('button#sendTonesButton'); const hangupButton = document.querySelector('button#hangupButton'); const durationInput = document.querySelector('input#duration'); const gapInput = document.querySelector('input#gap'); const tonesInput = document.querySelector('input#tones'); const durationValue = document.querySelector('span#durationValue'); const gapValue = document.querySelector('span#gapValue'); const sentTonesInput = document.querySelector('input#sentTones'); const dtmfStatusDiv = document.querySelector('div#dtmfStatus'); const audio = document.querySelector('audio'); let pc1; let pc2; let localStream; let dtmfSender; const offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 0 }; durationInput.oninput = () => { durationValue.textContent = durationInput.value; }; gapInput.oninput = () => { gapValue.textContent = gapInput.value; }; async function main() { addDialPadHandlers(); sendTonesButton.disabled = true; hangupButton.disabled = true; callButton.addEventListener('click', e => call()); sendTonesButton.addEventListener('click', e => handleSendTonesClick()); hangupButton.addEventListener('click', e => hangup()); } async function gotStream(stream) { console.log('Received local stream'); localStream = stream; const audioTracks = localStream.getAudioTracks(); if (audioTracks.length > 0) { console.log(`Using Audio device: ${audioTracks[0].label}`); } localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); console.log('Adding Local Stream to peer connection'); try { const offer = await pc1.createOffer(offerOptions); await gotLocalOffer(offer); } catch (e) { console.log('Failed to create session description:', e); } } async function call() { console.log('Starting call'); const servers = null; pc1 = new RTCPeerConnection(servers); console.log('Created local peer connection object pc1'); pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e)); pc2 = new RTCPeerConnection(servers); console.log('Created remote peer connection object pc2'); pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e)); pc2.addEventListener('track', e => gotRemoteStream(e)); console.log('Requesting local stream'); try { const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: false}); await gotStream(stream); } catch (e) { console.log('getUserMedia() error:', e); } callButton.disabled = true; hangupButton.disabled = false; sendTonesButton.disabled = false; } async function gotLocalOffer(desc) { console.log(`Offer from pc1\n${desc.sdp}`); pc1.setLocalDescription(desc); pc2.setRemoteDescription(desc); try { const answer = await pc2.createAnswer(); gotRemoteAnswer(answer); } catch (e) { console.log('Failed to create session description:', e); } } function gotRemoteAnswer(desc) { pc2.setLocalDescription(desc); console.log(`Answer from pc2:\n${desc.sdp}`); pc1.setRemoteDescription(desc); } function hangup() { console.log('Ending call'); pc1.close(); pc2.close(); pc1 = null; pc2 = null; localStream = null; dtmfSender = null; callButton.disabled = false; hangupButton.disabled = true; sendTonesButton.disabled = true; dtmfStatusDiv.textContent = 'DTMF deactivated'; } function gotRemoteStream(e) { if (audio.srcObject !== e.streams[0]) { audio.srcObject = e.streams[0]; console.log('Received remote stream'); if (!pc1.getSenders) { alert('This demo requires the RTCPeerConnection method getSenders() which is not support by this browser.'); return; } const senders = pc1.getSenders(); const audioSender = senders.find(sender => sender.track && sender.track.kind === 'audio'); if (!audioSender) { console.log('No local audio track to send DTMF with\n'); return; } if (!audioSender.dtmf) { alert('This demo requires DTMF which is not support by this browser.'); return; } dtmfSender = audioSender.dtmf; dtmfStatusDiv.textContent = 'DTMF available'; console.log('Got DTMFSender\n'); dtmfSender.ontonechange = dtmfOnToneChange; } } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } async function onIceCandidate(pc, event) { try { await getOtherPc(pc).addIceCandidate(event.candidate); console.log(`${getName(pc)} ICE candidate: ${event.candidate ? event.candidate.candidate : '(null)'}`); } catch (e) { console.log('Error adding ice candidate:', e); } } function dtmfOnToneChange(tone) { if (tone) { console.log(`Sent DTMF tone: ${tone.tone}`); sentTonesInput.value += `${tone.tone} `; } } function sendTones(tones) { // firefox doesn't implement canInsertDTMF, so assume it's always available // Ref: https://bugzilla.mozilla.org/show_bug.cgi?id=1623193 if (dtmfSender && (typeof (dtmfSender.canInsertDTMF) === 'undefined' || dtmfSender.canInsertDTMF)) { const duration = durationInput.value; const gap = gapInput.value; console.log('Tones, duration, gap: ', tones, duration, gap); dtmfSender.insertDTMF(tones, duration, gap); } } function handleSendTonesClick() { sendTones(tonesInput.value); } function addDialPadHandlers() { const dialPad = document.querySelector('div#dialPad'); const buttons = dialPad.querySelectorAll('button'); for (let i = 0; i !== buttons.length; ++i) { buttons[i].addEventListener('click', (event) => sendTones(event.target.textContent)); } } main(); ================================================ FILE: src/content/peerconnection/dtmf/js/test.js ================================================ /* * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/peerconnection/dtmf/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('peerconnection dtmf', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(async () => { await driver.get(url); await driver.findElement(webdriver.By.id('callButton')).click(); await driver.wait(() => driver.executeScript(() => { return localStream !== null; // eslint-disable-line no-undef })); await driver.wait(() => driver.executeScript(() => { return pc2 && pc2.connectionState === 'connected'; // eslint-disable-line no-undef })); }); it('sends the digit 1', async () => { await driver.findElement(webdriver.By.css('#dialPad>div:nth-child(1)>button:nth-child(1)')).click(); await driver.wait(driver.executeScript(() => { document.getElementById('sentTones').value.length !== 0; })); const sentTones = await driver.findElement(webdriver.By.id('sentTones')).getAttribute('value'); expect(sentTones).toBe('1 '); }); it('sends the digit 9', async () => { await driver.findElement(webdriver.By.css('#dialPad>div:nth-child(3)>button:nth-child(1)')).click(); await driver.wait(driver.executeScript(() => { document.getElementById('sentTones').value.length !== 0; })); const sentTones = await driver.findElement(webdriver.By.id('sentTones')).getAttribute('value'); expect(sentTones).toBe('9 '); }); it('sends the #', async () => { await driver.findElement(webdriver.By.css('#dialPad>div:nth-child(3)>button:nth-child(4)')).click(); await driver.wait(driver.executeScript(() => { document.getElementById('sentTones').value.length !== 0; })); const sentTones = await driver.findElement(webdriver.By.id('sentTones')).getAttribute('value'); expect(sentTones).toBe('# '); }); it('sends the A', async () => { await driver.findElement(webdriver.By.css('#dialPad>div:nth-child(4)>button:nth-child(1)')).click(); await driver.wait(driver.executeScript(() => { document.getElementById('sentTones').value.length !== 0; })); const sentTones = await driver.findElement(webdriver.By.id('sentTones')).getAttribute('value'); expect(sentTones).toBe('A '); }); }); ================================================ FILE: src/content/peerconnection/endtoend-encryption/index.html ================================================ Page move

The page has moved to: this page

================================================ FILE: src/content/peerconnection/multiple/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 0 0; width: 83px; } button#hangupButton { margin: 0; } video { margin: 0 0 20px 0; --width: 40%; width: var(--width); height: calc(var(--width) * 0.75); } video#video1 { margin: 0 20px 20px 0; } @media screen and (max-width: 400px) { button { margin: 0 11px 10px 0; } video { height: 90px; margin: 0 0 10px 0; width: calc(50% - 8px); } video#video1 { margin: 0 10px 10px 0; } } ================================================ FILE: src/content/peerconnection/multiple/index.html ================================================ Multiple peer connections

WebRTC samples Multiple peer connections

View the console to see logging and to inspect the MediaStream object localStream.

For more information about RTCPeerConnection, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/peerconnection/multiple/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); const hangupButton = document.getElementById('hangupButton'); callButton.disabled = true; hangupButton.disabled = true; startButton.onclick = start; callButton.onclick = call; hangupButton.onclick = hangup; const video1 = document.querySelector('video#video1'); const video2 = document.querySelector('video#video2'); const video3 = document.querySelector('video#video3'); // eslint-disable-next-line prefer-const let preferredVideoCodecMimeType = 'video/VP8'; let localStream; let pc1Local; let pc1Remote; let pc2Local; let pc2Remote; const supportsSetCodecPreferences = window.RTCRtpTransceiver && 'setCodecPreferences' in window.RTCRtpTransceiver.prototype; function maybeSetCodecPreferences(trackEvent) { if (!supportsSetCodecPreferences) return; if (trackEvent.track.kind === 'video' && preferredVideoCodecMimeType) { const {codecs} = RTCRtpReceiver.getCapabilities('video'); const selectedCodecIndex = codecs.findIndex(c => c.mimeType === preferredVideoCodecMimeType); const selectedCodec = codecs[selectedCodecIndex]; codecs.splice(selectedCodecIndex, 1); codecs.unshift(selectedCodec); trackEvent.transceiver.setCodecPreferences(codecs); } } async function start() { console.log('Requesting local stream'); startButton.disabled = true; localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); video1.srcObject = localStream; callButton.disabled = false; } async function call() { callButton.disabled = true; hangupButton.disabled = false; console.log('Starting calls'); const audioTracks = localStream.getAudioTracks(); const videoTracks = localStream.getVideoTracks(); if (audioTracks.length > 0) { console.log(`Using audio device: ${audioTracks[0].label}`); } if (videoTracks.length > 0) { console.log(`Using video device: ${videoTracks[0].label}`); } // Create an RTCPeerConnection via the polyfill. pc1Local = new RTCPeerConnection(); pc1Remote = new RTCPeerConnection(); pc1Remote.ontrack = e => gotRemoteStream(e, video2); console.log('pc1: created local and remote peer connection objects'); pc2Local = new RTCPeerConnection(); pc2Remote = new RTCPeerConnection(); pc2Remote.ontrack = e => gotRemoteStream(e, video3); console.log('pc2: created local and remote peer connection objects'); localStream.getTracks().forEach(track => { pc1Local.addTrack(track, localStream); pc2Local.addTrack(track, localStream); }); await Promise.all([ negotiate(pc1Local, pc1Remote), negotiate(pc2Local, pc2Remote), ]); } async function negotiate(localPc, remotePc) { localPc.onicecandidate = e => remotePc.addIceCandidate(e.candidate); remotePc.onicecandidate = e => localPc.addIceCandidate(e.candidate); await localPc.setLocalDescription(); await remotePc.setRemoteDescription(localPc.localDescription); await remotePc.setLocalDescription(); await localPc.setRemoteDescription(remotePc.localDescription); } function hangup() { console.log('Ending calls'); pc1Local.close(); pc1Remote.close(); pc2Local.close(); pc2Remote.close(); pc1Local = pc1Remote = null; pc2Local = pc2Remote = null; hangupButton.disabled = true; callButton.disabled = false; } function gotRemoteStream(e, videoObject) { maybeSetCodecPreferences(e); if (videoObject.srcObject !== e.streams[0]) { videoObject.srcObject = e.streams[0]; } } ================================================ FILE: src/content/peerconnection/multiple/js/test.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/peerconnection/multiple/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('multiple peerconnections', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('establishes multiple connections and hangs up', async () => { await driver.findElement(webdriver.By.id('startButton')).click(); await driver.wait(() => driver.executeScript(() => { return localStream !== null; // eslint-disable-line no-undef })); await driver.wait(() => driver.findElement(webdriver.By.id('callButton')).isEnabled()); await driver.findElement(webdriver.By.id('callButton')).click(); await Promise.all([ driver.wait(() => driver.executeScript(() => { return pc1Remote && pc1Remote.connectionState === 'connected'; // eslint-disable-line no-undef })), await driver.wait(() => driver.executeScript(() => { return pc2Remote && pc2Remote.connectionState === 'connected'; // eslint-disable-line no-undef })), ]); await Promise.all([ await driver.wait(() => driver.executeScript(() => { return document.getElementById('video2').readyState === HTMLMediaElement.HAVE_ENOUGH_DATA; })), await driver.wait(() => driver.executeScript(() => { return document.getElementById('video3').readyState === HTMLMediaElement.HAVE_ENOUGH_DATA; })), ]); await driver.findElement(webdriver.By.id('hangupButton')).click(); await Promise.all([ await driver.wait(() => driver.executeScript(() => { return pc1Remote === null; // eslint-disable-line no-undef })), await driver.wait(() => driver.executeScript(() => { return pc2Remote === null; // eslint-disable-line no-undef })), ]); }); }); ================================================ FILE: src/content/peerconnection/multiple-relay/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 20px 10px 0 0; width: 100px; } div#buttons { margin: 0 0 20px 0; } div#status { height: 2em; margin: 1em 0 0 0; } input#audio { margin: 0 0.5em 0 0; position: relative; top: -1px; } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); } ================================================ FILE: src/content/peerconnection/multiple-relay/index.html ================================================ Peer connection relay

WebRTC samples Peer connection relay

View source on GitHub
================================================ FILE: src/content/peerconnection/multiple-relay/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /* global VideoPipe */ const video1 = document.querySelector('video#video1'); const video2 = document.querySelector('video#video2'); const statusDiv = document.querySelector('div#status'); const audioCheckbox = document.querySelector('input#audio'); const startButton = document.querySelector('button#start'); const callButton = document.querySelector('button#call'); const insertRelayButton = document.querySelector('button#insertRelay'); const hangupButton = document.querySelector('button#hangup'); startButton.onclick = start; callButton.onclick = call; insertRelayButton.onclick = insertRelay; hangupButton.onclick = hangup; const pipes = []; let localStream; let remoteStream; function gotStream(stream) { console.log('Received local stream'); video1.srcObject = stream; localStream = stream; callButton.disabled = false; } function gotremoteStream(stream) { remoteStream = stream; video2.srcObject = stream; console.log('Received remote stream'); console.log(`${pipes.length} element(s) in chain`); statusDiv.textContent = `${pipes.length} element(s) in chain`; insertRelayButton.disabled = false; } function start() { console.log('Requesting local stream'); startButton.disabled = true; const options = audioCheckbox.checked ? {audio: true, video: true} : {audio: false, video: true}; navigator.mediaDevices .getUserMedia(options) .then(gotStream) .catch(function(e) { alert('getUserMedia() failed'); console.log('getUserMedia() error: ', e); }); } function call() { callButton.disabled = true; insertRelayButton.disabled = false; hangupButton.disabled = false; console.log('Starting call'); pipes.push(new VideoPipe(localStream, gotremoteStream)); } function insertRelay() { pipes.push(new VideoPipe(remoteStream, gotremoteStream)); insertRelayButton.disabled = true; } function hangup() { console.log('Ending call'); while (pipes.length > 0) { const pipe = pipes.pop(); pipe.close(); } insertRelayButton.disabled = true; hangupButton.disabled = true; callButton.disabled = false; } ================================================ FILE: src/content/peerconnection/munge-sdp/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 20px 0; vertical-align: top; width: 155px; } div#buttons { border-bottom: 1px solid #eee; margin: 1em 0 1em 0; padding: 0 0 1em 0; } div#local { margin: 0 20px 0 0; } div#preview { border-bottom: 1px solid #eee; margin: 0 0 1em 0; padding: 0 0 0.5em 0; } div#preview > div { display: inline-block; vertical-align: top; width: calc(50% - 12px); } div#select { margin: 0 0 1em 0; } div#selectSource { margin: 0 0 1em 0; } div.source { display: inline-block; margin: 0 0 1em 0; } form { margin: 0 0 1em 0; white-space: nowrap; } h2 { margin: 0 0 0.5em 0; } label { margin: 0 0.4em 0 0; } textarea { color: #444; font-size: 0.9em; font-weight: 300; height: 7.0em; padding: 5px; width: calc(100% - 10px); } video { height: 225px; } @media screen and (max-width: 550px) { button { font-weight: 500; height: 56px; line-height: 1.3em; margin: 0 7px 15px 0; width: 86px; } button:nth-child(3n+0) { margin: 0 0 15px 0; } video { height: 96px; } } @media screen and (max-width: 800px) { button { margin: 0 15px 15px 0; width: 155px; } div#selectSource { margin: 0 0 0.5em 0; } div.source { margin: 0 0 .2em 0; } select { margin: 0 1.5em 0 0; } textarea { font-size: 0.7em; } } @media screen and (max-width: 500px) { textarea { font-size: 0.5em; } } ================================================ FILE: src/content/peerconnection/munge-sdp/index.html ================================================ Munge SDP

WebRTC samples Munge SDP

Note: SDP "munging", i.e. modifying the SDP between createOffer/createAnswer and setLocalDescription is a nonstandard feature which may not work as expected. While some browsers support some modifications, the W3C standard forbids it.

Local

Offer SDP



Remote

Answer SDP

View the console to see logging.

The RTCPeerConnection objects pc1 and pc2 are in global scope, so you can inspect them in the console as well.

For more information about RTCPeerConnection, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/peerconnection/munge-sdp/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const getMediaButton = document.querySelector('button#getMedia'); const createPeerConnectionButton = document.querySelector('button#createPeerConnection'); const createOfferButton = document.querySelector('button#createOffer'); const setOfferButton = document.querySelector('button#setOffer'); const createAnswerButton = document.querySelector('button#createAnswer'); const setAnswerButton = document.querySelector('button#setAnswer'); const hangupButton = document.querySelector('button#hangup'); let dataChannelDataReceived; getMediaButton.onclick = getMedia; createPeerConnectionButton.onclick = createPeerConnection; createOfferButton.onclick = createOffer; setOfferButton.onclick = setOffer; createAnswerButton.onclick = createAnswer; setAnswerButton.onclick = setAnswer; hangupButton.onclick = hangup; const offerSdpTextarea = document.querySelector('div#local textarea'); const answerSdpTextarea = document.querySelector('div#remote textarea'); const localVideo = document.querySelector('div#local video'); const remoteVideo = document.querySelector('div#remote video'); let pc1; let pc2; let localStream; let sendChannel; let receiveChannel; const dataChannelOptions = {ordered: true}; let dataChannelCounter = 0; let sendDataLoop; const offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 1 }; async function getMedia() { getMediaButton.disabled = true; if (localStream) { localVideo.srcObject = null; localStream.getTracks().forEach(track => track.stop()); } console.log('Requesting local stream'); try { const userMedia = await navigator.mediaDevices.getUserMedia({audio: true, video: true}); gotStream(userMedia); } catch (e) { console.log('navigator.getUserMedia error: ', e); } } function gotStream(stream) { console.log('Received local stream'); localVideo.srcObject = stream; localStream = stream; createPeerConnectionButton.disabled = false; } function createPeerConnection() { createPeerConnectionButton.disabled = true; createOfferButton.disabled = false; console.log('Starting call'); const videoTracks = localStream.getVideoTracks(); const audioTracks = localStream.getAudioTracks(); if (videoTracks.length > 0) { console.log(`Using video device: ${videoTracks[0].label}`); } if (audioTracks.length > 0) { console.log(`Using audio device: ${audioTracks[0].label}`); } pc1 = new RTCPeerConnection(); console.log('Created local peer connection object pc1'); pc1.onicecandidate = e => onIceCandidate(pc1, e); sendChannel = pc1.createDataChannel('sendDataChannel', dataChannelOptions); sendChannel.onopen = onSendChannelStateChange; sendChannel.onclose = onSendChannelStateChange; sendChannel.onerror = onSendChannelStateChange; pc2 = new RTCPeerConnection(); console.log('Created remote peer connection object pc2'); pc2.onicecandidate = e => onIceCandidate(pc2, e); pc2.ontrack = gotRemoteStream; pc2.ondatachannel = receiveChannelCallback; localStream.getTracks() .forEach(track => pc1.addTrack(track, localStream)); console.log('Adding Local Stream to peer connection'); } function onSetSessionDescriptionSuccess() { console.log('Set session description success.'); } function onSetSessionDescriptionError(error) { console.log(`Failed to set session description: ${error.toString()}`); document.getElementById('munge-error').innerText = error.toString(); } async function createOffer() { try { const offer = await pc1.createOffer(offerOptions); gotDescription1(offer); } catch (e) { onCreateSessionDescriptionError(e); } } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); } async function setOffer() { // Restore the SDP from the textarea. Ensure we use CRLF which is what is generated // even though https://tools.ietf.org/html/rfc4566#section-5 requires // parsers to handle both LF and CRLF. const sdp = offerSdpTextarea.value .split('\n') .map(l => l.trim()) .join('\r\n'); const offer = { type: 'offer', sdp: sdp }; console.log(`Modified Offer from pc1\n${sdp}`); try { await pc1.setLocalDescription(offer); onSetSessionDescriptionSuccess(); setOfferButton.disabled = true; } catch (e) { onSetSessionDescriptionError(e); return; } try { await pc2.setRemoteDescription(offer); onSetSessionDescriptionSuccess(); createAnswerButton.disabled = false; } catch (e) { onSetSessionDescriptionError(e); return; } } function gotDescription1(description) { offerSdpTextarea.disabled = false; offerSdpTextarea.value = description.sdp; createOfferButton.disabled = true; setOfferButton.disabled = false; } async function createAnswer() { // Since the 'remote' side has no media stream we need // to pass in the right constraints in order for it to // accept the incoming offer of audio and video. try { const answer = await pc2.createAnswer(); gotDescription2(answer); } catch (e) { onCreateSessionDescriptionError(e); } } async function setAnswer() { setAnswerButton.disabled = false; // Restore the SDP from the textarea. Ensure we use CRLF which is what is generated // even though https://tools.ietf.org/html/rfc4566#section-5 requires // parsers to handle both LF and CRLF. const sdp = answerSdpTextarea.value .split('\n') .map(l => l.trim()) .join('\r\n'); const answer = { type: 'answer', sdp: sdp }; try { // eslint-disable-next-line no-unused-vars const ignore = await pc2.setLocalDescription(answer); onSetSessionDescriptionSuccess(); setAnswerButton.disabled = true; } catch (e) { onSetSessionDescriptionError(e); return; } console.log(`Modified Answer from pc2\n${sdp}`); try { // eslint-disable-next-line no-unused-vars const ignore = await pc1.setRemoteDescription(answer); onSetSessionDescriptionSuccess(); } catch (e) { onSetSessionDescriptionError(e); return; } hangupButton.disabled = false; createOfferButton.disabled = false; } function gotDescription2(description) { answerSdpTextarea.disabled = false; answerSdpTextarea.value = description.sdp; createAnswerButton.disabled = true; setAnswerButton.disabled = false; } function sendData() { if (sendChannel.readyState === 'open') { sendChannel.send(dataChannelCounter); console.log(`DataChannel send counter: ${dataChannelCounter}`); dataChannelCounter++; } } function hangup() { remoteVideo.srcObject = null; console.log('Ending call'); localStream.getTracks().forEach(track => track.stop()); sendChannel.close(); if (receiveChannel) { receiveChannel.close(); } pc1.close(); pc2.close(); pc1 = null; pc2 = null; offerSdpTextarea.disabled = true; answerSdpTextarea.disabled = true; getMediaButton.disabled = false; createPeerConnectionButton.disabled = true; createOfferButton.disabled = true; setOfferButton.disabled = true; createAnswerButton.disabled = true; setAnswerButton.disabled = true; hangupButton.disabled = true; } function gotRemoteStream(e) { if (remoteVideo.srcObject !== e.streams[0]) { remoteVideo.srcObject = e.streams[0]; console.log('Received remote stream'); } } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } async function onIceCandidate(pc, event) { try { // eslint-disable-next-line no-unused-vars const ignore = await getOtherPc(pc).addIceCandidate(event.candidate); onAddIceCandidateSuccess(pc); } catch (e) { onAddIceCandidateError(pc, e); } console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess() { console.log('AddIceCandidate success.'); } function onAddIceCandidateError(error) { console.log(`Failed to add Ice Candidate: ${error.toString()}`); } function receiveChannelCallback(event) { console.log('Receive Channel Callback'); receiveChannel = event.channel; receiveChannel.onmessage = onReceiveMessageCallback; receiveChannel.onopen = onReceiveChannelStateChange; receiveChannel.onclose = onReceiveChannelStateChange; } function onReceiveMessageCallback(event) { dataChannelDataReceived = event.data; console.log(`DataChannel receive counter: ${dataChannelDataReceived}`); } function onSendChannelStateChange() { const readyState = sendChannel.readyState; console.log(`Send channel state is: ${readyState}`); if (readyState === 'open') { sendDataLoop = setInterval(sendData, 1000); } else { clearInterval(sendDataLoop); } } function onReceiveChannelStateChange() { const readyState = receiveChannel.readyState; console.log(`Receive channel state is: ${readyState}`); } ================================================ FILE: src/content/peerconnection/munge-sdp/js/test.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/peerconnection/munge-sdp/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('peerconnection sdp munging', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('establishes a connection and allows hangup and new offer', async () => { await driver.findElement(webdriver.By.id('getMedia')).click(); await driver.wait(() => driver.findElement(webdriver.By.id('createPeerConnection')).isEnabled()); await driver.findElement(webdriver.By.id('createPeerConnection')).click(); await driver.wait(() => driver.findElement(webdriver.By.id('createOffer')).isEnabled()); await driver.findElement(webdriver.By.id('createOffer')).click(); await driver.wait(() => driver.findElement(webdriver.By.id('setOffer')).isEnabled()); await driver.findElement(webdriver.By.id('setOffer')).click(); await driver.wait(() => driver.findElement(webdriver.By.id('createAnswer')).isEnabled()); await driver.findElement(webdriver.By.id('createAnswer')).click(); await driver.wait(() => driver.findElement(webdriver.By.id('setAnswer')).isEnabled()); await driver.findElement(webdriver.By.id('setAnswer')).click(); await driver.wait(() => driver.findElement(webdriver.By.id('hangup')).isEnabled()); await driver.wait(() => driver.findElement(webdriver.By.id('createOffer')).isEnabled()); await Promise.all([ await driver.wait(() => driver.executeScript(() => { return pc1 && pc1.connectionState === 'connected'; // eslint-disable-line no-undef })), await driver.wait(() => driver.executeScript(() => { return pc2 && pc2.connectionState === 'connected'; // eslint-disable-line no-undef })), ]); }); // TODO: add test to ensure the text fields are properly filled }); ================================================ FILE: src/content/peerconnection/negotiate-timing/css/main.css ================================================ /* * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 0 0; width: 83px; } button#hangupButton { margin: 0; } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); margin: 0 0 20px 0; vertical-align: top; } video#localVideo { margin: 0 20px 20px 0; } @media screen and (max-width: 400px) { button { width: 83px; margin: 0 11px 10px 0; } video { height: 90px; margin: 0 0 10px 0; width: calc(50% - 7px); } video#localVideo { margin: 0 10px 20px 0; } } ================================================ FILE: src/content/peerconnection/negotiate-timing/index.html ================================================ Peer connection - Renegotiate

WebRTC samples Peer connection negotiation timing

Video sections after renegotiating:

View the console to see logging. The MediaStream object localStream, and the RTCPeerConnection objects pc1 and pc2 are in global scope, so you can inspect them in the console as well.

Log goes here

View source on GitHub
================================================ FILE: src/content/peerconnection/negotiate-timing/js/main.js ================================================ /* * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); const renegotiateButton = document.getElementById('renegotiateButton'); const hangupButton = document.getElementById('hangupButton'); const log = document.getElementById('log'); const videoSectionsField = document.getElementById('videoSections'); callButton.disabled = true; hangupButton.disabled = true; renegotiateButton.disabled = true; startButton.onclick = start; callButton.onclick = call; renegotiateButton.onclick = renegotiate; hangupButton.onclick = hangup; let startTime; const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); let audioTransceiver; let audioImpairmentAtStart = 0; let result; // Allows configuration of bundlePolicy et al. // eslint-disable-next-line prefer-const let configuration = null; // Preferring a certain codec is an expert option without GUI. // eslint-disable-next-line prefer-const let preferredVideoCodecMimeType = undefined; // e.g. 'video/VP8' localVideo.addEventListener('loadedmetadata', function() { console.log(`Local video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.addEventListener('loadedmetadata', function() { console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.onresize = () => { console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`); console.warn('RESIZE', remoteVideo.videoWidth, remoteVideo.videoHeight); // We'll use the first onsize callback as an indication that video has started // playing out. if (startTime) { const elapsedTime = window.performance.now() - startTime; console.log(`Setup time: ${elapsedTime.toFixed(3)}ms`); startTime = null; } }; let localStream; let pc1; let pc2; function logToScreen(text) { log.append(document.createElement('br')); log.append(text); } function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } async function start() { console.log('Requesting local stream'); startButton.disabled = true; const stream = await navigator.mediaDevices .getUserMedia({ audio: true, video: true }); console.log('Received local stream'); localVideo.srcObject = stream; localStream = stream; callButton.disabled = false; } async function runOfferAnswer() { const startTime = performance.now(); const result = {}; const offer = await pc1.createOffer(); const markTime1 = performance.now(); result.callerCreateOffer = markTime1 - startTime; await pc1.setLocalDescription(offer); const markTime2 = performance.now(); result.callerSetLocalDescription = markTime2 - markTime1; await pc2.setRemoteDescription(offer); const markTime3 = performance.now(); result.calleeSetRemoteDescription = markTime3 - markTime2; const answer = await pc2.createAnswer(); const markTime4 = performance.now(); result.calleeCreateAnswer = markTime4 - markTime3; await pc1.setRemoteDescription(answer); const markTime5 = performance.now(); result.callerSetRemoteDescription = markTime5 - markTime4; await pc2.setLocalDescription(answer); const markTime6 = performance.now(); result.calleeSetLocalDescription = markTime6 - markTime5; result.elapsedTime = markTime6 - startTime; return result; } async function call() { callButton.disabled = true; renegotiateButton.disabled = false; hangupButton.disabled = false; console.log('Starting call'); startTime = window.performance.now(); const audioTracks = localStream.getAudioTracks(); if (audioTracks.length > 0) { console.log(`Using audio device: ${audioTracks[0].label}`); } pc1 = new RTCPeerConnection(configuration); console.log('Created local peer connection object pc1'); pc1.onicecandidate = e => onIceCandidate(pc1, e); pc2 = new RTCPeerConnection(configuration); console.log('Created remote peer connection object pc2'); pc2.onicecandidate = e => onIceCandidate(pc2, e); pc1.oniceconnectionstatechange = e => onIceStateChange(pc1, e); pc2.oniceconnectionstatechange = e => onIceStateChange(pc2, e); pc2.addEventListener('track', gotRemoteStream, {once: true}); if (preferredVideoCodecMimeType) { pc2.ontrack = (e) => { if (e.track.kind === 'video') { const {codecs} = RTCRtpReceiver.getCapabilities('video'); const selectedCodecIndex = codecs.findIndex(c => c.mimeType === preferredVideoCodecMimeType); const selectedCodec = codecs[selectedCodecIndex]; codecs.splice(selectedCodecIndex, 1); codecs.unshift(selectedCodec); e.transceiver.setCodecPreferences(codecs); } }; } localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); console.log('Added local stream to pc1'); await runOfferAnswer(); console.log('Initial negotiation complete'); } function gotRemoteStream(e) { console.log('gotRemoteStream', e.track, e.streams[0]); if (e.streams[0]) { // reset srcObject to work around minor bugs in Chrome and Edge. remoteVideo.srcObject = null; remoteVideo.srcObject = e.streams[0]; } } async function onIceCandidate(pc, event) { if (event.candidate) { console.log(`${getName(pc)} emitted ICE candidate for index ${event.candidate.sdpMLineIndex}:\n${event.candidate.candidate}`); } else { console.log(`${getName(pc)} ICE NULL candidate`); } await getOtherPc(pc).addIceCandidate(event.candidate); console.log(`${getName(pc)} addIceCandidate success`); } function onIceStateChange(pc, event) { if (pc) { console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`); console.log('ICE state change event, state: ', pc.iceConnectionState); } } function adjustTransceiverCounts(pc, videoCount) { const currentVideoTransceivers = pc.getTransceivers().filter(tr => tr.receiver.track.kind == 'video'); const currentVideoCount = currentVideoTransceivers.length; if (currentVideoCount < videoCount) { console.log('Adding ' + (videoCount - currentVideoCount) + ' transceivers'); for (let i = currentVideoCount; i < videoCount; ++i) { pc.addTransceiver('video'); } } else if (currentVideoCount > videoCount) { console.log('Stopping ' + (currentVideoCount - videoCount) + ' transceivers'); for (let i = videoCount; i < currentVideoCount; ++i) { currentVideoTransceivers[i].stop(); } } else { console.log(`No adjustment, video count is ${currentVideoCount}, target was ${videoCount}`); } } async function getAudioImpairment(audioTransceiver) { const stats = await audioTransceiver.receiver.getStats(); let currentImpairment; stats.forEach(stat => { if (stat.type == 'inbound-rtp') { currentImpairment = stat.concealedSamples; } }); console.log('Found impairment value ', currentImpairment); return currentImpairment; } async function baselineAudioImpairment(pc) { audioTransceiver = pc.getTransceivers().find(tr => tr.receiver.track.kind == 'audio'); console.log('Found audio transceiver'); audioImpairmentAtStart = await getAudioImpairment(audioTransceiver); } async function measureAudioImpairment(pc) { const startTime = performance.now(); const audioImpairmentNow = await getAudioImpairment(audioTransceiver); console.log('Measurement took ' + (performance.now() - startTime) + ' msec'); return audioImpairmentNow - audioImpairmentAtStart; } async function renegotiate() { renegotiateButton.disabled = true; adjustTransceiverCounts(pc1, parseInt(videoSectionsField.value)); await baselineAudioImpairment(pc2); const previousVideoTransceiverCount = pc2.getTransceivers().filter(tr => tr.receiver.track.kind == 'video').length; result = await runOfferAnswer(); console.log(`Renegotiate finished after ${result.elapsedTime} milliseconds`); const currentVideoTransceiverCount = pc2.getTransceivers().filter(tr => tr.receiver.track.kind == 'video').length; result.audioImpairment = await measureAudioImpairment(pc2); logToScreen(`Negotiation from ${previousVideoTransceiverCount} to ${currentVideoTransceiverCount} video transceivers took ${result.elapsedTime.toFixed(2)} milliseconds, audio impairment ${result.audioImpairment}`); console.log('Results: ', JSON.stringify(result, ' ', 2)); renegotiateButton.disabled = false; } function hangup() { console.log('Ending call'); pc1.close(); pc2.close(); pc1 = null; pc2 = null; console.log('Releasing camera'); const videoTracks = localStream.getVideoTracks(); videoTracks.forEach(videoTrack => { videoTrack.stop(); localStream.removeTrack(videoTrack); }); localVideo.srcObject = null; hangupButton.disabled = true; callButton.disabled = true; renegotiateButton.disabled = true; startButton.disabled = false; } ================================================ FILE: src/content/peerconnection/negotiate-timing/js/test.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/peerconnection/negotiate-timing/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('peerconnection with negotiation timing', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('establishes a connection, renegotiates and hangs up', async () => { await driver.findElement(webdriver.By.id('startButton')).click(); await driver.wait(() => driver.executeScript(() => { return localStream !== null; // eslint-disable-line no-undef })); await driver.wait(() => driver.findElement(webdriver.By.id('callButton')).isEnabled()); await driver.findElement(webdriver.By.id('callButton')).click(); await Promise.all([ await driver.wait(() => driver.executeScript(() => { return pc1 && pc1.connectionState === 'connected'; // eslint-disable-line no-undef })), await driver.wait(() => driver.executeScript(() => { return pc2 && pc2.connectionState === 'connected'; // eslint-disable-line no-undef })), ]); await driver.wait(() => driver.executeScript(() => { return document.getElementById('remoteVideo').readyState === HTMLMediaElement.HAVE_ENOUGH_DATA; })); await driver.wait(() => driver.findElement(webdriver.By.id('renegotiateButton')).isEnabled()); await driver.findElement(webdriver.By.id('renegotiateButton')).click(); await driver.wait(() => driver.executeScript(() => { return document.getElementById('log').innerText !== 'Log goes here'; })); const logText = await driver.findElement(webdriver.By.id('log')).getAttribute('innerText'); expect(logText.split('\n').length).toBe(2); await driver.findElement(webdriver.By.id('hangupButton')).click(); await driver.wait(() => driver.executeScript(() => { return pc1 === null; // eslint-disable-line no-undef })); }); }); ================================================ FILE: src/content/peerconnection/pc1/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 0 0; width: 83px; } button#hangupButton { margin: 0; } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); margin: 0 0 20px 0; vertical-align: top; } video#localVideo { margin: 0 20px 20px 0; } div.box { margin: 1em; } @media screen and (max-width: 400px) { button { width: 83px; margin: 0 11px 10px 0; } video { height: 90px; margin: 0 0 10px 0; width: calc(50% - 7px); } video#localVideo { margin: 0 10px 20px 0; } } ================================================ FILE: src/content/peerconnection/pc1/index.html ================================================ Peer connection

WebRTC samples Peer connection

This sample shows how to setup a connection between two peers using RTCPeerConnection.

View the console to see logging. The MediaStream object localStream, and the RTCPeerConnection objects pc1 and pc2 are in global scope, so you can inspect them in the console as well.

For more information about RTCPeerConnection, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/peerconnection/pc1/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); const hangupButton = document.getElementById('hangupButton'); callButton.disabled = true; hangupButton.disabled = true; startButton.addEventListener('click', start); callButton.addEventListener('click', call); hangupButton.addEventListener('click', hangup); let startTime; const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); localVideo.addEventListener('loadedmetadata', function() { console.log(`Local video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.addEventListener('loadedmetadata', function() { console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.addEventListener('resize', () => { console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight} - Time since pageload ${performance.now().toFixed(0)}ms`); // We'll use the first onsize callback as an indication that video has started // playing out. if (startTime) { const elapsedTime = window.performance.now() - startTime; console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms'); startTime = null; } }); let localStream; let pc1; let pc2; const offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 1 }; function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } async function start() { console.log('Requesting local stream'); startButton.disabled = true; try { const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true}); console.log('Received local stream'); localVideo.srcObject = stream; localStream = stream; callButton.disabled = false; } catch (e) { alert(`getUserMedia() error: ${e.name}`); } } async function call() { callButton.disabled = true; hangupButton.disabled = false; console.log('Starting call'); startTime = window.performance.now(); const videoTracks = localStream.getVideoTracks(); const audioTracks = localStream.getAudioTracks(); if (videoTracks.length > 0) { console.log(`Using video device: ${videoTracks[0].label}`); } if (audioTracks.length > 0) { console.log(`Using audio device: ${audioTracks[0].label}`); } const configuration = {}; console.log('RTCPeerConnection configuration:', configuration); pc1 = new RTCPeerConnection(configuration); console.log('Created local peer connection object pc1'); pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e)); pc2 = new RTCPeerConnection(configuration); console.log('Created remote peer connection object pc2'); pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e)); pc1.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1, e)); pc2.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2, e)); pc2.addEventListener('track', gotRemoteStream); localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); console.log('Added local stream to pc1'); try { console.log('pc1 createOffer start'); const offer = await pc1.createOffer(offerOptions); await onCreateOfferSuccess(offer); } catch (e) { onCreateSessionDescriptionError(e); } } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); } async function onCreateOfferSuccess(desc) { console.log(`Offer from pc1\n${desc.sdp}`); console.log('pc1 setLocalDescription start'); try { await pc1.setLocalDescription(desc); onSetLocalSuccess(pc1); } catch (e) { onSetSessionDescriptionError(); } console.log('pc2 setRemoteDescription start'); try { await pc2.setRemoteDescription(desc); onSetRemoteSuccess(pc2); } catch (e) { onSetSessionDescriptionError(); } console.log('pc2 createAnswer start'); // Since the 'remote' side has no media stream we need // to pass in the right constraints in order for it to // accept the incoming offer of audio and video. try { const answer = await pc2.createAnswer(); await onCreateAnswerSuccess(answer); } catch (e) { onCreateSessionDescriptionError(e); } } function onSetLocalSuccess(pc) { console.log(`${getName(pc)} setLocalDescription complete`); } function onSetRemoteSuccess(pc) { console.log(`${getName(pc)} setRemoteDescription complete`); } function onSetSessionDescriptionError(error) { console.log(`Failed to set session description: ${error.toString()}`); } function gotRemoteStream(e) { if (remoteVideo.srcObject !== e.streams[0]) { remoteVideo.srcObject = e.streams[0]; console.log('pc2 received remote stream'); } } async function onCreateAnswerSuccess(desc) { console.log(`Answer from pc2:\n${desc.sdp}`); console.log('pc2 setLocalDescription start'); try { await pc2.setLocalDescription(desc); onSetLocalSuccess(pc2); } catch (e) { onSetSessionDescriptionError(e); } console.log('pc1 setRemoteDescription start'); try { await pc1.setRemoteDescription(desc); onSetRemoteSuccess(pc1); } catch (e) { onSetSessionDescriptionError(e); } } async function onIceCandidate(pc, event) { try { await (getOtherPc(pc).addIceCandidate(event.candidate)); onAddIceCandidateSuccess(pc); } catch (e) { onAddIceCandidateError(pc, e); } console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess(pc) { console.log(`${getName(pc)} addIceCandidate success`); } function onAddIceCandidateError(pc, error) { console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`); } function onIceStateChange(pc, event) { if (pc) { console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`); console.log('ICE state change event: ', event); } } function hangup() { console.log('Ending call'); pc1.close(); pc2.close(); pc1 = null; pc2 = null; hangupButton.disabled = true; callButton.disabled = false; } ================================================ FILE: src/content/peerconnection/pc1/js/test.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/peerconnection/pc1/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('simple peerconnection', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('establishes a connection and hangs up', async () => { await driver.findElement(webdriver.By.id('startButton')).click(); await driver.wait(() => driver.executeScript(() => { return localStream !== null; // eslint-disable-line no-undef })); await driver.wait(() => driver.findElement(webdriver.By.id('callButton')).isEnabled()); await driver.findElement(webdriver.By.id('callButton')).click(); await Promise.all([ await driver.wait(() => driver.executeScript(() => { return pc1 && pc1.connectionState === 'connected'; // eslint-disable-line no-undef })), await driver.wait(() => driver.executeScript(() => { return pc2 && pc2.connectionState === 'connected'; // eslint-disable-line no-undef })), ]); await driver.wait(() => driver.executeScript(() => { return document.getElementById('remoteVideo').readyState === HTMLMediaElement.HAVE_ENOUGH_DATA; })); await driver.findElement(webdriver.By.id('hangupButton')).click(); await driver.wait(() => driver.executeScript(() => { return pc1 === null; // eslint-disable-line no-undef })); }); }); ================================================ FILE: src/content/peerconnection/per-frame-callback/css/main.css ================================================ /* * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); } button { margin: 0 20px 0 0; width: 96px; } video#localVideo { margin: 0 20px 20px 0; } div.label { display: inline-block; font-weight: 400; width: 120px; } div.graph-container { float: left; margin: 0.5em; width: calc(50% - 1em); } a#viewSource { clear: both; } ================================================ FILE: src/content/peerconnection/per-frame-callback/index.html ================================================ Peer connection and requestVideoFrameCallback()

WebRTC samples Peer connection and requestVideoFrameCallback()

Local capture delay (converted to milliseconds)
Network delay (milliseconds)

Render delay (milliseconds)
Processing time (converted to milliseconds)

For more information about requestVideoFrameCallback, see Perform efficient per-video-frame operations on video with requestVideoFrameCallback().

View source on GitHub
================================================ FILE: src/content/peerconnection/per-frame-callback/js/main.js ================================================ /* * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* global TimelineDataSeries, TimelineGraphView */ 'use strict'; const remoteVideo = document.querySelector('video#remoteVideo'); const localVideo = document.querySelector('video#localVideo'); const callButton = document.querySelector('button#callButton'); const hangupButton = document.querySelector('button#hangupButton'); const bandwidthSelector = document.querySelector('select#bandwidth'); hangupButton.disabled = true; callButton.onclick = call; hangupButton.onclick = hangup; if (!('requestVideoFrameCallback' in HTMLVideoElement.prototype)) { document.getElementById('notsupported').style.display = 'block'; callButton.disabled = true; } let localDelayGraph; let localDelaySeries; let maxLocalDelay = -1; localVideo.requestVideoFrameCallback(function rVFC(now, metaData) { // For graph purposes, take the maximum over a window. maxLocalDelay = Math.max(1000 * (metaData.expectedDisplayTime - metaData.captureTime), maxLocalDelay); if (metaData.presentedFrames % windowSize !== 0) { localVideo.requestVideoFrameCallback(rVFC); return; } // The graph library does not like the performance.now() style `now`. localDelaySeries.addPoint(Date.now(), maxLocalDelay); localDelayGraph.setDataSeries([localDelaySeries]); localDelayGraph.updateEndDate(); maxLocalDelay = -1; localVideo.requestVideoFrameCallback(rVFC); }); let processingGraph; let processingSeries; let timeGraph; let timeSeries; let networkDelayGraph; let networkDelaySeries; let maxProcessingDuration = -1; let maxRenderTime = -1; let maxNetworkDelay = -1; const windowSize = 30; remoteVideo.requestVideoFrameCallback(function rVFC(now, metaData) { // For graph purposes, take the maximum over a window. maxProcessingDuration = Math.max(1000 * metaData.processingDuration, maxProcessingDuration); maxRenderTime = Math.max(metaData.expectedDisplayTime - metaData.receiveTime, maxRenderTime); // Note: captureTime is currently only present when there are bidirectional streams. maxNetworkDelay = Math.max(metaData.receiveTime - metaData.captureTime, maxNetworkDelay); if (metaData.presentedFrames % windowSize !== 0) { remoteVideo.requestVideoFrameCallback(rVFC); return; } // The graph library does not like the performance.now() style `now`. processingSeries.addPoint(Date.now(), maxProcessingDuration); processingGraph.setDataSeries([processingSeries]); processingGraph.updateEndDate(); timeSeries.addPoint(Date.now(), maxRenderTime); timeGraph.setDataSeries([timeSeries]); timeGraph.updateEndDate(); networkDelaySeries.addPoint(Date.now(), maxNetworkDelay); networkDelayGraph.setDataSeries([networkDelaySeries]); networkDelayGraph.updateEndDate(); maxProcessingDuration = -1; maxRenderTime = -1; maxNetworkDelay = -1; maxLocalDelay = -1; remoteVideo.requestVideoFrameCallback(rVFC); }); // Mostly copied from pc1/bandwidth sample. let pc1; let pc2; let localStream; function gotStream(stream) { hangupButton.disabled = false; console.log('Received local stream'); localStream = stream; localVideo.srcObject = stream; localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); // Currently the captureTime on the remote end requires bidirectional video. localStream.getTracks().forEach(track => pc2.addTrack(track, localStream)); console.log('Adding Local Stream to peer connection'); pc1.createOffer().then( gotDescription1, onCreateSessionDescriptionError ); processingSeries = new TimelineDataSeries(); processingGraph = new TimelineGraphView('processingGraph', 'processingCanvas'); processingGraph.updateEndDate(); timeSeries = new TimelineDataSeries(); timeGraph = new TimelineGraphView('timeGraph', 'timeCanvas'); timeGraph.updateEndDate(); networkDelaySeries = new TimelineDataSeries(); networkDelayGraph = new TimelineGraphView('networkDelayGraph', 'networkDelayCanvas'); networkDelayGraph.updateEndDate(); localDelaySeries = new TimelineDataSeries(); localDelayGraph = new TimelineGraphView('localDelayGraph', 'localDelayCanvas'); } function onCreateSessionDescriptionError(error) { console.log('Failed to create session description: ' + error.toString()); } function call() { callButton.disabled = true; console.log('Starting call'); pc1 = new RTCPeerConnection(); console.log('Created local peer connection object pc1'); pc1.onicecandidate = onIceCandidate.bind(pc1); pc2 = new RTCPeerConnection(); console.log('Created remote peer connection object pc2'); pc2.onicecandidate = onIceCandidate.bind(pc2); pc2.ontrack = gotRemoteStream; console.log('Requesting local stream'); navigator.mediaDevices.getUserMedia({video: true}) .then(gotStream) .catch(e => alert('getUserMedia() error: ' + e.name)); } function gotDescription1(desc) { console.log('Offer from pc1 \n' + desc.sdp); pc1.setLocalDescription(desc).then( () => { pc2.setRemoteDescription(desc) .then(() => pc2.createAnswer().then(gotDescription2, onCreateSessionDescriptionError), onSetSessionDescriptionError); }, onSetSessionDescriptionError ); } function gotDescription2(desc) { pc2.setLocalDescription(desc).then( () => { console.log('Answer from pc2 \n' + desc.sdp); return pc1.setRemoteDescription(desc); }, onSetSessionDescriptionError ); } function hangup() { console.log('Ending call'); localStream.getTracks().forEach(track => track.stop()); pc1.close(); pc2.close(); pc1 = null; pc2 = null; hangupButton.disabled = true; callButton.disabled = false; bandwidthSelector.disabled = true; } function gotRemoteStream(e) { if (remoteVideo.srcObject !== e.streams[0]) { remoteVideo.srcObject = e.streams[0]; console.log('Received remote stream'); } } function getOtherPc(pc) { return pc === pc1 ? pc2 : pc1; } function getName(pc) { return pc === pc1 ? 'pc1' : 'pc2'; } function onIceCandidate(event) { getOtherPc(this) .addIceCandidate(event.candidate) .then(onAddIceCandidateSuccess) .catch(onAddIceCandidateError); console.log(`${getName(this)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess() { console.log('AddIceCandidate success.'); } function onAddIceCandidateError(error) { console.log('Failed to add ICE Candidate: ' + error.toString()); } function onSetSessionDescriptionError(error) { console.log('Failed to set session description: ' + error.toString()); } ================================================ FILE: src/content/peerconnection/perfect-negotiation/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 0 0; width: 83px; } button#hangupButton { margin: 0; } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); margin: 0 0 20px 0; vertical-align: top; } video#localVideo { margin: 0 20px 20px 0; } div.box { margin: 1em; } @media screen and (max-width: 400px) { button { width: 83px; margin: 0 11px 10px 0; } video { height: 90px; margin: 0 0 10px 0; width: calc(50% - 7px); } video#localVideo { margin: 0 10px 20px 0; } } ================================================ FILE: src/content/peerconnection/perfect-negotiation/index.html ================================================ Perfect Negotiation

WebRTC samples Perfect Negotiation

This sample shows how to setup a connection between two peers using RTCPeerConnection with the Perfect Negotiation usage pattern.

Perfect Negotiation supports both endpoints sending offers. The pattern intelligently handles the situation of "glare" (both peers making an offer at the same time, causing a collision) by having one peer be "polite" and the other peer be "impolite". In the event of an offer collision, the polite peer rolls back its offer in order to process the impolite peer's incoming offer. Once back to the stable signaling state, the polite peer's onnegotiationneeded fires again and a follow-up O/A is completed.

Click both peers' Start button to create local streams. Then press the Swap Sending Track button to modify which transceiver is sending; this will be negotiated and displayed as a remote track on the other peer's iframe.

The JavaScript console shows logs for the negotiation steps.

For more information about RTCPeerConnection, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/peerconnection/perfect-negotiation/js/main.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; import {peer} from './peer.js'; const politeIframe = document.getElementById('polite'); const impoliteIframe = document.getElementById('impolite'); let counter = 0; /** * @param {Window} target * @param {string} cmd */ export async function run(target, cmd) { const id = `result${counter++}`; target.postMessage({run: {cmd, id}}, '*'); return new Promise(resolve => void window.addEventListener('message', function listen({data}) { if (!(id in data)) return; window.removeEventListener('message', listen); resolve(data[id]); })); } /** * @param {number} r1 Video 1 Red channel level [0-1] * @param {number} g1 Video 1 Green channel level [0-1] * @param {number} b1 Video 1 Blue channel level [0-1] * @param {number} r2 Video 2 Red channel level [0-1] * @param {number} g2 Video 2 Green channel level [0-1] * @param {number} b2 Video 2 Blue channel level [0-1] */ export function startLocalVideo(r1, g1, b1, r2, g2, b2) { const whiteNoise = (width, height, r, g, b) => { const canvas = Object.assign(document.createElement('canvas'), {width, height}); const ctx = canvas.getContext('2d'); ctx.fillRect(0, 0, width, height); const p = ctx.getImageData(0, 0, width, height); const draw = () => { for (let i = 0; i < p.data.length; i++) { const color = Math.random() * 255; p.data[i++] = color * r; p.data[i++] = color * g; p.data[i++] = color * b; } ctx.putImageData(p, 0, 0); requestAnimationFrame(draw); }; requestAnimationFrame(draw); return canvas.captureStream(); }; const localVideo1 = document.getElementById('localVideo1'); localVideo1.srcObject = whiteNoise(32, 32, r1, g1, b1); localVideo1.play(); const localVideo2 = document.getElementById('localVideo2'); localVideo2.srcObject = whiteNoise(32, 32, r2, g2, b2); localVideo2.play(); } /** * @param {HTMLIFrameElement} el * @param {boolean} polite * @param {number} r1 Video 1 Red channel level [0-1] * @param {number} g1 Video 1 Green channel level [0-1] * @param {number} b1 Video 1 Blue channel level [0-1] * @param {number} r2 Video 2 Red channel level [0-1] * @param {number} g2 Video 2 Green channel level [0-1] * @param {number} b2 Video 2 Blue channel level [0-1] */ async function setupIframe(el, polite, r1, g1, b1, r2, g2, b2) { el.srcdoc = `

${polite ? 'Polite' : 'Impolite'} Peer's iframe

`; await new Promise(resolve => el.onload = resolve); } export async function swapOnBoth(politeFirst) { // eslint-disable-line no-unused-vars if (politeFirst) { run(politeIframe.contentWindow, 'swapTransceivers'); run(impoliteIframe.contentWindow, 'swapTransceivers'); } else { run(impoliteIframe.contentWindow, 'swapTransceivers'); run(politeIframe.contentWindow, 'swapTransceivers'); } } async function setupIframes() { await setupIframe(politeIframe, true, 0, 1, 0, 0, 1, 1); await setupIframe(impoliteIframe, false, 1, 0, 0, 1, 0, 1); } window.run = run; window.swapOnBoth = swapOnBoth; window.peer = peer; export {peer}; setupIframes(); ================================================ FILE: src/content/peerconnection/perfect-negotiation/js/peer.js ================================================ /* * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /** * Establishes a Peer Connection with `other` using the Perfect Negotiation pattern. * @param {Window} other * @param {boolean} polite * @param {function(Error): void} fail * @return {RTCPeerConnection} Peer Connection */ function peer(other, polite, fail = undefined) { // eslint-disable-line no-unused-vars if (!fail) fail = e => void send(window.parent, {error: `${e.name}: ${e.message}`}); const send = (target, msg) => void target.postMessage(JSON.parse(JSON.stringify(msg)), '*'); const log = str => void console.log(`[${polite ? 'POLITE' : 'IMPOLITE'}] ${str}`); const assert_equals = !window.assert_equals ? (a, b, msg) => a === b || void fail(new Error(`${msg} expected ${b} but got ${a}`)) : window.assert_equals; const pc = new RTCPeerConnection(); const localVideo1 = document.getElementById('localVideo1'); const localVideo2 = document.getElementById('localVideo2'); const remoteVideo = document.getElementById('remoteVideo'); const transceiversForSending = []; const commands = { async swapTransceivers() { log('swapTransceivers'); const stream1 = localVideo1.srcObject; const stream2 = localVideo2.srcObject; if (transceiversForSending.length == 0) { // This is the first time swapTransceivers is called. // Add the initial transceivers, which are remembered for future swaps. transceiversForSending.push( pc.addTransceiver(stream1.getTracks()[0], {streams: [stream1], direction: 'sendonly'})); transceiversForSending.push( pc.addTransceiver('video', {streams: [stream2], direction: 'inactive'})); return; } // We have sent before. Swap which transceiver is the sending one. if (transceiversForSending[0].direction == 'sendonly') { transceiversForSending[0].direction = 'inactive'; transceiversForSending[0].sender.replaceTrack(null); transceiversForSending[1].direction = 'sendonly'; transceiversForSending[1].sender.replaceTrack(stream2.getTracks()[0]); } else { transceiversForSending[1].direction = 'inactive'; transceiversForSending[1].sender.replaceTrack(null); transceiversForSending[0].direction = 'sendonly'; transceiversForSending[0].sender.replaceTrack(stream1.getTracks()[0]); } }, }; try { pc.ontrack = e => { log('ontrack'); remoteVideo.srcObject = new MediaStream(); remoteVideo.srcObject.addTrack(e.track); }; pc.onicecandidate = ({candidate}) => void send(other, {candidate}); let makingOffer = false; let ignoreOffer = false; let srdAnswerPending = false; pc.onnegotiationneeded = async () => { try { log('SLD due to negotiationneeded'); assert_equals(pc.signalingState, 'stable', 'negotiationneeded always fires in stable state'); assert_equals(makingOffer, false, 'negotiationneeded not already in progress'); makingOffer = true; await pc.setLocalDescription(); assert_equals(pc.signalingState, 'have-local-offer', 'negotiationneeded not racing with onmessage'); assert_equals(pc.localDescription.type, 'offer', 'negotiationneeded SLD worked'); send(other, {description: pc.localDescription}); } catch (e) { fail(e); } finally { makingOffer = false; } }; window.onmessage = async ({data: {description, candidate, run}}) => { try { if (description) { // If we have a setRemoteDescription() answer operation pending, then // we will be "stable" by the time the next setRemoteDescription() is // executed, so we count this being stable when deciding whether to // ignore the offer. const isStable = pc.signalingState == 'stable' || (pc.signalingState == 'have-local-offer' && srdAnswerPending); ignoreOffer = description.type == 'offer' && !polite && (makingOffer || !isStable); if (ignoreOffer) { log('glare - ignoring offer'); return; } srdAnswerPending = description.type == 'answer'; log(`SRD(${description.type})`); await pc.setRemoteDescription(description); srdAnswerPending = false; if (description.type == 'offer') { assert_equals(pc.signalingState, 'have-remote-offer', 'Remote offer'); assert_equals(pc.remoteDescription.type, 'offer', 'SRD worked'); log('SLD to get back to stable'); await pc.setLocalDescription(); assert_equals(pc.signalingState, 'stable', 'onmessage not racing with negotiationneeded'); assert_equals(pc.localDescription.type, 'answer', 'onmessage SLD worked'); send(other, {description: pc.localDescription}); } else { assert_equals(pc.remoteDescription.type, 'answer', 'Answer was set'); assert_equals(pc.signalingState, 'stable', 'answered'); pc.dispatchEvent(new Event('negotiated')); } } else if (candidate) { try { await pc.addIceCandidate(candidate); } catch (e) { if (!ignoreOffer) throw e; } } else if (run) { send(window.parent, {[run.id]: await commands[run.cmd]() || 0}); } } catch (e) { fail(e); } }; } catch (e) { fail(e); } return pc; } export {peer}; ================================================ FILE: src/content/peerconnection/pr-answer/index.html ================================================ PeerConnection PRANSWER Demo

WebRTC samples Use pranswer when setting up a peer connection


View the console to see logging and to inspect the MediaStream object localStream.

View source on GitHub
================================================ FILE: src/content/peerconnection/pr-answer/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const vid1 = document.getElementById('vid1'); const vid2 = document.getElementById('vid2'); const callButton = document.getElementById('callButton'); const acceptButton = document.getElementById('acceptButton'); const hangUpButton = document.getElementById('hangUpButton'); callButton.addEventListener('click', start); acceptButton.addEventListener('click', accept); hangUpButton.addEventListener('click', stop); callButton.disabled = true; acceptButton.disabled = true; hangUpButton.disabled = true; let pc1 = null; let pc2 = null; let localStream; const remoteStream = new MediaStream(); const offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 1 }; function gotStream(stream) { console.log('Received local stream'); vid1.srcObject = stream; localStream = stream; callButton.disabled = false; } navigator.mediaDevices .getUserMedia({ audio: true, video: true }) .then(gotStream) .catch(e => alert(`getUserMedia() error: ${e}`)); function start() { callButton.disabled = true; acceptButton.disabled = false; hangUpButton.disabled = false; console.log('Starting Call'); const videoTracks = localStream.getVideoTracks(); const audioTracks = localStream.getAudioTracks(); if (videoTracks.length > 0) { console.log(`Using Video device: ${videoTracks[0].label}`); } if (audioTracks.length > 0) { console.log(`Using Audio device: ${audioTracks[0].label}`); } const servers = null; pc1 = new RTCPeerConnection(servers); console.log('Created local peer connection object pc1'); pc1.onicecandidate = e => onIceCandidate(pc1, e); pc2 = new RTCPeerConnection(servers); console.log('Created remote peer connection object pc2'); pc2.onicecandidate = e => onIceCandidate(pc2, e); pc2.ontrack = gotRemoteStream; localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); console.log('Adding Local Stream to peer connection'); pc1.createOffer(offerOptions).then(gotDescription1, onCreateSessionDescriptionError); } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); stop(); } function onCreateAnswerError(error) { console.log(`Failed to set createAnswer: ${error.toString()}`); stop(); } function onSetLocalDescriptionError(error) { console.log(`Failed to set setLocalDescription: ${error.toString()}`); stop(); } function onSetLocalDescriptionSuccess() { console.log('localDescription success.'); } function gotDescription1(desc) { pc1.setLocalDescription(desc).then( onSetLocalDescriptionSuccess, onSetLocalDescriptionError ); console.log(`Offer from pc1\n${desc.sdp}`); pc2.setRemoteDescription(desc); // Since the 'remote' side has no media stream we need // to pass in the right constraints in order for it to // accept the incoming offer of audio and video. pc2.createAnswer().then(gotDescription2, onCreateSessionDescriptionError); } function gotDescription2(desc) { // Provisional answer, set a=inactive & set sdp type to pranswer. desc.sdp = desc.sdp.replace(/a=recvonly/g, 'a=inactive'); desc.type = 'pranswer'; pc2.setLocalDescription(desc).then(onSetLocalDescriptionSuccess, onSetLocalDescriptionError); console.log(`Pranswer from pc2\n${desc.sdp}`); pc1.setRemoteDescription(desc); } function gotDescription3(desc) { // Final answer, setting a=recvonly & sdp type to answer. desc.sdp = desc.sdp.replace(/a=inactive/g, 'a=recvonly'); desc.type = 'answer'; pc2.setLocalDescription(desc).then(onSetLocalDescriptionSuccess, onSetLocalDescriptionError); console.log(`Answer from pc2\n${desc.sdp}`); pc1.setRemoteDescription(desc); } function accept() { pc2.createAnswer().then(gotDescription3, onCreateAnswerError); acceptButton.disabled = true; callButton.disabled = true; } function stop() { console.log('Ending Call' + '\n\n'); pc1.close(); pc2.close(); pc1 = null; pc2 = null; acceptButton.disabled = true; callButton.disabled = false; hangUpButton.disabled = true; } function gotRemoteStream(e) { vid2.srcObject = remoteStream; remoteStream.addTrack(e.track, remoteStream); } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function onIceCandidate(pc, event) { getOtherPc(pc) .addIceCandidate(event.candidate) .then(() => onAddIceCandidateSuccess(pc), err => onAddIceCandidateError(pc, err)); console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess() { console.log('AddIceCandidate success.'); } function onAddIceCandidateError(error) { console.log(`Failed to add Ice Candidate: ${error.toString()}`); } ================================================ FILE: src/content/peerconnection/restart-ice/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 0 0; width: 83px; } button#hangupButton { margin: 0; } video { --width: 100%; width: var(--width); height: calc(var(--width) * 0.75); margin: 0; } div#video > div { display: inline-block; margin: 0 5px 0 0; vertical-align: top; width: calc(50% - 22px); } @media screen and (max-width: 400px) { button { width: 83px; margin: 0 11px 10px 0; } } ================================================ FILE: src/content/peerconnection/restart-ice/index.html ================================================ ICE Restart

WebRTC samples Peer connection

Not connected.

Not connected.

View the console to see logging. The MediaStream object localStream, and the RTCPeerConnection objects pc1 and pc2 are in global scope, so you can inspect them in the console as well.

For more information about RTCPeerConnection, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/peerconnection/restart-ice/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); const restartButton = document.getElementById('restartButton'); const hangupButton = document.getElementById('hangupButton'); callButton.disabled = true; hangupButton.disabled = true; restartButton.disabled = true; startButton.onclick = start; callButton.onclick = call; hangupButton.onclick = hangup; restartButton.onclick = restart; let startTime; const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); localVideo.addEventListener('loadedmetadata', function() { console.log(`Local video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.addEventListener('loadedmetadata', function() { console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); const useSelectedCandidatePairChange = window.RTCIceTransport && 'onselectedcandidatepairchange' in RTCIceTransport.prototype; remoteVideo.onresize = () => { console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`); // We'll use the first onsize callback as an indication that video has started // playing out. if (startTime) { const elapsedTime = window.performance.now() - startTime; console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms'); startTime = null; // Have run these functions again in order to get the getStats() reports // with type candidatePair and populate the candidate id // elements. checkStats(pc1); checkStats(pc2); } }; let localStream; let pc1; let pc2; const offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 1 }; function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } function gotStream(stream) { console.log('Received local stream'); localVideo.srcObject = stream; localStream = stream; callButton.disabled = false; } function start() { console.log('Requesting local stream'); startButton.disabled = true; navigator.mediaDevices .getUserMedia({ audio: true, video: true }) .then(gotStream) .catch(e => alert(`getUserMedia() error: ${e.name}`)); } // Simulate an ice restart. function restart() { restartButton.disabled = true; offerOptions.iceRestart = true; console.log('pc1 createOffer restart'); pc1.createOffer(offerOptions).then(onCreateOfferSuccess, onCreateSessionDescriptionError); } function call() { callButton.disabled = true; hangupButton.disabled = false; console.log('Starting call'); startTime = window.performance.now(); const videoTracks = localStream.getVideoTracks(); const audioTracks = localStream.getAudioTracks(); if (videoTracks.length > 0) { console.log(`Using video device: ${videoTracks[0].label}`); } if (audioTracks.length > 0) { console.log(`Using audio device: ${audioTracks[0].label}`); } const servers = null; pc1 = window.pc1 = new RTCPeerConnection(servers); console.log('Created local peer connection object pc1'); pc1.onicecandidate = e => onIceCandidate(pc1, e); pc2 = window.pc2 = new RTCPeerConnection(servers); console.log('Created remote peer connection object pc2'); pc2.onicecandidate = e => onIceCandidate(pc2, e); pc1.oniceconnectionstatechange = e => { onIceStateChange(pc1, e); if (pc1 && pc1.iceConnectionState === 'connected') { restartButton.disabled = false; } }; pc2.oniceconnectionstatechange = e => onIceStateChange(pc2, e); pc2.ontrack = gotRemoteStream; localStream.getTracks().forEach(track => pc1.addTrack(track, localStream) ); console.log('Added local stream to pc1'); console.log('pc1 createOffer start'); pc1.createOffer(offerOptions).then(onCreateOfferSuccess, onCreateSessionDescriptionError); } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); } function onCreateOfferSuccess(desc) { console.log(`Offer from pc1\n${desc.sdp}`); console.log('pc1 setLocalDescription start'); pc1.setLocalDescription(desc).then(() => onSetLocalSuccess(pc1), onSetSessionDescriptionError); console.log('pc2 setRemoteDescription start'); pc2.setRemoteDescription(desc).then(() => onSetRemoteSuccess(pc2), onSetSessionDescriptionError); console.log('pc2 createAnswer start'); // Since the 'remote' side has no media stream we need // to pass in the right constraints in order for it to // accept the incoming offer of audio and video. pc2.createAnswer().then(onCreateAnswerSuccess, onCreateSessionDescriptionError); } function onSetLocalSuccess(pc) { console.log(`${getName(pc)} setLocalDescription complete`); } function onSetRemoteSuccess(pc) { console.log(`${getName(pc)} setRemoteDescription complete`); } function onSetSessionDescriptionError(error) { console.log(`Failed to set session description: ${error.toString()}`); } function gotRemoteStream(e) { if (remoteVideo.srcObject !== e.streams[0]) { remoteVideo.srcObject = e.streams[0]; console.log('pc2 received remote stream'); } } function onCreateAnswerSuccess(desc) { console.log(`Answer from pc2:\n${desc.sdp}`); console.log('pc2 setLocalDescription start'); pc2.setLocalDescription(desc).then(() => onSetLocalSuccess(pc2), onSetSessionDescriptionError); console.log('pc1 setRemoteDescription start'); pc1.setRemoteDescription(desc).then(() => onSetRemoteSuccess(pc1), onSetSessionDescriptionError); if (useSelectedCandidatePairChange) { pc1.getSenders()[0].transport.iceTransport.onselectedcandidatepairchange = () => { checkStats(pc1); if (pc1.iceConnectionState === 'connected') { restartButton.disabled = false; } }; pc2.getSenders()[0].transport.iceTransport.onselectedcandidatepairchange = () => { checkStats(pc2); }; } } function onIceCandidate(pc, event) { getOtherPc(pc) .addIceCandidate(event.candidate) .then(() => onAddIceCandidateSuccess(pc), err => onAddIceCandidateError(pc, err)); console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess(pc) { console.log(`${getName(pc)} addIceCandidate success`); } function onAddIceCandidateError(pc, error) { console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`); } function onIceStateChange(pc, event) { if (pc) { console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`); console.log('ICE state change event: ', event); if (!useSelectedCandidatePairChange) { if (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') { checkStats(pc); } } } } function checkStats(pc) { pc.getStats(null).then(results => { // figure out the peer's ip let activeCandidatePair = null; let remoteCandidate = null; // Search for the candidate pair, spec-way first. results.forEach(report => { if (report.type === 'transport') { activeCandidatePair = results.get(report.selectedCandidatePairId); } }); // Fallback for Firefox. if (!activeCandidatePair) { results.forEach(report => { if (report.type === 'candidate-pair' && report.state === 'succeeded' && report.selected) { activeCandidatePair = report; } }); } if (activeCandidatePair && activeCandidatePair.remoteCandidateId) { results.forEach(report => { if (report.type === 'remote-candidate' && report.id === activeCandidatePair.remoteCandidateId) { remoteCandidate = report; } }); } console.log(remoteCandidate); if (remoteCandidate && remoteCandidate.id) { // TODO: update a div showing the remote ip/port? document.getElementById(pc === pc1 ? 'localCandidateId' : 'remoteCandidateId').textContent = remoteCandidate.id; } }); } function hangup() { console.log('Ending call'); pc1.close(); pc2.close(); pc1 = null; pc2 = null; hangupButton.disabled = true; restartButton.disabled = true; callButton.disabled = false; } ================================================ FILE: src/content/peerconnection/restart-ice/js/test.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/peerconnection/restart-ice/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('peerconnection ice restart', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('establishes a connection and changes candidates on restart', async () => { await driver.findElement(webdriver.By.id('startButton')).click(); await driver.wait(() => driver.executeScript(() => { return localStream !== null; // eslint-disable-line no-undef })); await driver.wait(() => driver.findElement(webdriver.By.id('callButton')).isEnabled()); await driver.findElement(webdriver.By.id('callButton')).click(); await driver.wait(() => driver.executeScript(() => { return pc1 && pc1.connectionState === 'connected'; // eslint-disable-line no-undef })); await driver.wait(() => driver.executeScript(() => { return pc2 && pc2.connectionState === 'connected'; // eslint-disable-line no-undef })); await driver.wait(() => driver.executeScript(() => { return document.getElementById('remoteVideo').readyState === HTMLMediaElement.HAVE_ENOUGH_DATA; })); await driver.wait(() => driver.findElement(webdriver.By.id('restartButton')).isEnabled()); const firstCandidateIds = await Promise.all([ await driver.findElement(webdriver.By.id('localCandidateId')).getAttribute('innerText'), await driver.findElement(webdriver.By.id('remoteCandidateId')).getAttribute('innerText'), ]); await driver.wait(() => driver.findElement(webdriver.By.id('restartButton')).isEnabled()); await driver.findElement(webdriver.By.id('restartButton')).click(); await driver.wait(() => driver.findElement(webdriver.By.id('restartButton')).isEnabled()); const secondCandidateIds = await Promise.all([ await driver.findElement(webdriver.By.id('localCandidateId')).getAttribute('innerText'), await driver.findElement(webdriver.By.id('remoteCandidateId')).getAttribute('innerText'), ]); expect(secondCandidateIds[0]).not.toBe(firstCandidateIds[0]); expect(secondCandidateIds[1]).not.toBe(firstCandidateIds[1]); }); }); ================================================ FILE: src/content/peerconnection/states/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 20px 0; width: 86.5px; } div#buttons { border-bottom: 1px solid #eee; margin: 0 0 20px 0; } div#states { border-bottom: 1px solid #eee; } div#states > div { margin: 0 0 20px 0; min-height: 24px; /* to cope with Unicode character size :^| */ } div.label { display: inline-block; font-weight: 400; width: 111px; } div.value { display: inline-block; } video { margin: 0 0 20px 0; --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); } video#video1 { margin: 0 20px 20px 0; } @media screen and (min-width: 730px) { video { height: 231px; } } ================================================ FILE: src/content/peerconnection/states/index.html ================================================ Peer connection: states

WebRTC samples Peer connection: states

PC1 signaling state:
PC1 ICE state:
PC1 connection state:
PC2 signaling state:
PC2 ICE state:
PC2 connection state:

View the console to see logging. The MediaStream object localStream, and the RTCPeerConnection objects pc1 and pc2 are in global scope, so you can inspect them in the console as well.

For more information about RTCPeerConnection, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/peerconnection/states/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const video1 = document.querySelector('video#video1'); const video2 = document.querySelector('video#video2'); const startButton = document.querySelector('button#startButton'); const callButton = document.querySelector('button#callButton'); const hangupButton = document.querySelector('button#hangupButton'); startButton.disabled = false; callButton.disabled = true; hangupButton.disabled = true; startButton.onclick = start; callButton.onclick = call; hangupButton.onclick = hangup; const pc1SignalStateDiv = document.querySelector('div#pc1SignalState'); const pc1IceStateDiv = document.querySelector('div#pc1IceState'); const pc1ConnStateDiv = document.querySelector('div#pc1ConnState'); const pc2SignalStateDiv = document.querySelector('div#pc2SignalState'); const pc2IceStateDiv = document.querySelector('div#pc2IceState'); const pc2ConnStateDiv = document.querySelector('div#pc2ConnState'); let localStream; let pc1; let pc2; const offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 1 }; function gotStream(stream) { console.log('Received local stream'); video1.srcObject = stream; localStream = stream; callButton.disabled = false; } function start() { console.log('Requesting local stream'); startButton.disabled = true; navigator.mediaDevices .getUserMedia({ audio: true, video: true }) .then(gotStream) .catch(e => alert('getUserMedia() error: ', e.name)); } function call() { callButton.disabled = true; hangupButton.disabled = false; console.log('Starting call'); const videoTracks = localStream.getVideoTracks(); const audioTracks = localStream.getAudioTracks(); if (videoTracks.length > 0) { console.log(`Using Video device: ${videoTracks[0].label}`); } if (audioTracks.length > 0) { console.log(`Using Audio device: ${audioTracks[0].label}`); } const servers = null; pc1 = new RTCPeerConnection(servers); console.log('Created local peer connection object pc1'); pc1.onsignalingstatechange = stateCallback1; pc1SignalStateDiv.textContent = pc1.signalingState; pc1IceStateDiv.textContent = pc1.iceConnectionState; pc1ConnStateDiv.textContent = pc1.connectionState; pc1.oniceconnectionstatechange = iceStateCallback1; pc1.onconnectionstatechange = connStateCallback1; pc1.onicecandidate = e => onIceCandidate(pc1, e); pc2 = new RTCPeerConnection(servers); console.log('Created remote peer connection object pc2'); pc2.onsignalingstatechange = stateCallback2; pc2SignalStateDiv.textContent = pc2.signalingState; pc2IceStateDiv.textContent = pc2.iceConnectionState; pc2ConnStateDiv.textContent = pc2.connectionState; pc2.oniceconnectionstatechange = iceStateCallback2; pc2.onconnectionstatechange = connStateCallback2; pc2.onicecandidate = e => onIceCandidate(pc2, e); pc2.ontrack = gotRemoteStream; localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); console.log('Adding Local Stream to peer connection'); pc1.createOffer(offerOptions).then(gotDescription1, onCreateSessionDescriptionError); } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); } function gotDescription1(description) { pc1.setLocalDescription(description); console.log(`Offer from pc1:\n${description.sdp}`); pc2.setRemoteDescription(description); pc2.createAnswer().then(gotDescription2, onCreateSessionDescriptionError); } function gotDescription2(description) { pc2.setLocalDescription(description); console.log(`Answer from pc2\n${description.sdp}`); pc1.setRemoteDescription(description); } function hangup() { console.log('Ending call'); pc1.close(); pc2.close(); pc1SignalStateDiv.textContent += ` => ${pc1.signalingState}`; pc2SignalStateDiv.textContent += ` => ${pc2.signalingState}`; pc1IceStateDiv.textContent += ` => ${pc1.iceConnectionState}`; pc2IceStateDiv.textContent += ` => ${pc2.iceConnectionState}`; pc1ConnStateDiv.textContent += ` => ${pc1.connectionState}`; pc2ConnStateDiv.textContent += ` => ${pc2.connectionState}`; pc1 = null; pc2 = null; hangupButton.disabled = true; callButton.disabled = false; } function gotRemoteStream(e) { if (video2.srcObject !== e.streams[0]) { video2.srcObject = e.streams[0]; console.log('Got remote stream'); } } function stateCallback1() { let state; if (pc1) { state = pc1.signalingState; console.log(`pc1 state change callback, state: ${state}`); pc1SignalStateDiv.textContent += ` => ${state}`; } } function stateCallback2() { let state; if (pc2) { state = pc2.signalingState; console.log(`pc2 state change callback, state: ${state}`); pc2SignalStateDiv.textContent += ` => ${state}`; } } function iceStateCallback1() { let iceState; if (pc1) { iceState = pc1.iceConnectionState; console.log(`pc1 ICE connection state change callback, state: ${iceState}`); pc1IceStateDiv.textContent += ` => ${iceState}`; } } function iceStateCallback2() { let iceState; if (pc2) { iceState = pc2.iceConnectionState; console.log(`pc2 ICE connection state change callback, state: ${iceState}`); pc2IceStateDiv.textContent += ` => ${iceState}`; } } function connStateCallback1() { if (pc1) { const {connectionState} = pc1; console.log(`pc1 connection state change callback, state: ${connectionState}`); pc1ConnStateDiv.textContent += ` => ${connectionState}`; } } function connStateCallback2() { if (pc2) { const {connectionState} = pc2; console.log(`pc2 connection state change callback, state: ${connectionState}`); pc2ConnStateDiv.textContent += ` => ${connectionState}`; } } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function onIceCandidate(pc, event) { getOtherPc(pc) .addIceCandidate(event.candidate) .then(() => onAddIceCandidateSuccess(pc), err => onAddIceCandidateError(pc, err)); console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess() { console.log('AddIceCandidate success.'); } function onAddIceCandidateError(error) { console.log(`Failed to add Ice Candidate: ${error.toString()}`); } ================================================ FILE: src/content/peerconnection/states/js/test.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/peerconnection/states/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('peerconnection states', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('establishes a connection and hangs up', async () => { await driver.findElement(webdriver.By.id('startButton')).click(); await driver.wait(() => driver.executeScript(() => { return localStream !== null; // eslint-disable-line no-undef })); await driver.wait(() => driver.findElement(webdriver.By.id('callButton')).isEnabled()); await driver.findElement(webdriver.By.id('callButton')).click(); await Promise.all([ await driver.wait(() => driver.executeScript(() => { return pc1 && pc1.connectionState === 'connected'; // eslint-disable-line no-undef })), await driver.wait(() => driver.executeScript(() => { return pc2 && pc2.connectionState === 'connected'; // eslint-disable-line no-undef })), ]); await driver.wait(() => driver.executeScript(() => { return document.getElementById('video2').readyState === HTMLMediaElement.HAVE_ENOUGH_DATA; })); const pc1States = { signaling: await driver.findElement(webdriver.By.id('pc1SignalState')).getAttribute('innerText'), ice: await driver.findElement(webdriver.By.id('pc1IceState')).getAttribute('innerText'), connection: await driver.findElement(webdriver.By.id('pc1ConnState')).getAttribute('innerText'), }; expect(pc1States.signaling).toBe('stable => have-local-offer => stable'); expect(pc1States.ice).toBe('new => checking => connected'); expect(pc1States.connection).toBe('new => connecting => connected'); const pc2States = { signaling: await driver.findElement(webdriver.By.id('pc2SignalState')).getAttribute('innerText'), ice: await driver.findElement(webdriver.By.id('pc2IceState')).getAttribute('innerText'), connection: await driver.findElement(webdriver.By.id('pc2ConnState')).getAttribute('innerText'), }; expect(pc2States.signaling).toBe('stable => have-remote-offer => stable'); expect(pc2States.ice).toBe('new => checking => connected'); expect(pc2States.connection).toBe('new => connecting => connected'); await driver.findElement(webdriver.By.id('hangupButton')).click(); await driver.wait(() => driver.executeScript(() => { return pc1 === null; // eslint-disable-line no-undef })); }); }); ================================================ FILE: src/content/peerconnection/trickle-ice/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 20px 10px 0 0; width: 130px; } button#gather { display: block; } section#iceServers input { margin: 0 0 10px; width: 260px; } section#iceConstraints label { margin: 0 1em 0 0; } section#iceServers label { display: inline-block; width: 150px; } section#iceOptions label { display: inline-block; width: 200px; } select#servers { font-size: 1em; padding: 5px; width: 420px; } div#iceTransports span { margin: 0 1em 0 0; } table#candidates { font-size: 0.7em; overflow-y: auto; text-align: right; width: 100%; } th { font-weight: bold; } th:nth-child(3),td:nth-child(3) { text-align: left } th:nth-child(6),td:nth-child(6) { text-align: left } .gray { color: gray } #poolValue { display: inline-block; width: 30px } #getUserMediaPermissions { display: none; } #error-note { display: none; } ================================================ FILE: src/content/peerconnection/trickle-ice/index.html ================================================ Trickle ICE

WebRTC samples Trickle ICE

This page tests the trickle ICE functionality in a WebRTC implementation. It creates a PeerConnection with the specified ICEServers, and then starts candidate gathering for a session with a single audio stream. As candidates are gathered, they are displayed in the text box below, along with an indication when candidate gathering is complete.

Note that if no getUserMedia permissions for this origin are persisted only candidates from a single interface will be gathered in Chrome. See the RTCWEB IP address handling recommendations draft for details.You have given permission, candidate from multiple interface will be gathered.

Individual STUN and TURN servers can be added using the Add server / Remove server controls below; in addition, the type of candidates released to the application can be controlled via the IceTransports constraint.

If you test a STUN server, it works if you can gather a candidate with type "srflx". If you test a TURN server, it works if you can gather a candidate with type "relay".

If you test just a single TURN/UDP server, this page even allows you to detect when you are using the wrong credential to authenticate.

ICE servers

ICE options

all relay
Time Type Foundation Protocol Address Port Priority URL (if present) relayProtocol (if present)
Note: errors from onicecandidateerror above are not necessarily fatal. For example an IPv6 DNS lookup may fail but relay candidates can still be gathered via IPv4.
View source on GitHub
================================================ FILE: src/content/peerconnection/trickle-ice/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const addButton = document.querySelector('button#add'); const candidateTBody = document.querySelector('tbody#candidatesBody'); const gatherButton = document.querySelector('button#gather'); const passwordInput = document.querySelector('input#password'); const removeButton = document.querySelector('button#remove'); const resetButton = document.querySelector('button#reset'); const servers = document.querySelector('select#servers'); const urlInput = document.querySelector('input#url'); const usernameInput = document.querySelector('input#username'); const getUserMediaInput = document.querySelector('input#getUserMedia'); addButton.onclick = addServer; gatherButton.onclick = start; removeButton.onclick = removeServer; resetButton.onclick = (e) => { window.localStorage.clear(); document.querySelectorAll('select#servers option').forEach(option => option.remove()); const serversSelect = document.querySelector('select#servers'); setDefaultServer(serversSelect); }; let begin; let pc; let stream; let candidates; const allServersKey = 'servers'; function setDefaultServer(serversSelect) { const option = document.createElement('option'); option.value = '{"urls":["stun:stun.l.google.com:19302"]}'; option.text = 'stun:stun.l.google.com:19302'; option.ondblclick = selectServer; serversSelect.add(option); } function writeServersToLocalStorage() { const serversSelect = document.querySelector('select#servers'); const allServers = JSON.stringify(Object.values(serversSelect.options).map(o => JSON.parse(o.value))); window.localStorage.setItem(allServersKey, allServers); } function readServersFromLocalStorage() { document.querySelectorAll('select#servers option').forEach(option => option.remove()); const serversSelect = document.querySelector('select#servers'); const storedServers = window.localStorage.getItem(allServersKey); if (storedServers === null || storedServers === '') { setDefaultServer(serversSelect); } else { JSON.parse(storedServers).forEach((server, key) => { const o = document.createElement('option'); o.value = JSON.stringify(server); o.text = server.urls[0]; o.ondblclick = selectServer; serversSelect.add(o); }); } } function selectServer(event) { const option = event.target; const value = JSON.parse(option.value); urlInput.value = value.urls[0]; usernameInput.value = value.username || ''; passwordInput.value = value.credential || ''; } function addServer() { if (urlInput.value === '' && usernameInput.value === '' && passwordInput.value === '') { // Ignore since this leads to invisible items being added to the list. console.warn('Not adding empty ICE server input'); return; } // Store the ICE server as a stringified JSON object in option.value. const option = document.createElement('option'); const iceServer = { urls: [urlInput.value], username: usernameInput.value, credential: passwordInput.value }; option.value = JSON.stringify(iceServer); option.text = `${urlInput.value} `; const username = usernameInput.value; const password = passwordInput.value; if (username || password) { option.text += (` [${username}:${password}]`); } option.ondblclick = selectServer; servers.add(option); urlInput.value = usernameInput.value = passwordInput.value = ''; writeServersToLocalStorage(); } function removeServer() { for (let i = servers.options.length - 1; i >= 0; --i) { if (servers.options[i].selected) { servers.remove(i); } } writeServersToLocalStorage(); } async function start() { // Clean out the table. while (candidateTBody.firstChild) { candidateTBody.removeChild(candidateTBody.firstChild); } gatherButton.disabled = true; if (getUserMediaInput.checked) { stream = await navigator.mediaDevices.getUserMedia({audio: true}); } getUserMediaInput.disabled = true; // Read the values from the input boxes. const iceServers = []; for (let i = 0; i < servers.length; ++i) { iceServers.push(JSON.parse(servers[i].value)); } const transports = document.getElementsByName('transports'); let iceTransports; for (let i = 0; i < transports.length; ++i) { if (transports[i].checked) { iceTransports = transports[i].value; break; } } // Create a PeerConnection with no streams, but force a m=audio line. const config = { iceServers: iceServers, iceTransportPolicy: iceTransports, }; const offerOptions = {offerToReceiveAudio: 1}; // Whether we gather IPv6 candidates. // Whether we only gather a single set of candidates for RTP and RTCP. console.log(`Creating new PeerConnection with config=${JSON.stringify(config)}`); const errDiv = document.getElementById('error'); errDiv.innerText = ''; let desc; try { pc = new RTCPeerConnection(config); pc.onicecandidate = iceCallback; pc.onicegatheringstatechange = gatheringStateChange; pc.onicecandidateerror = iceCandidateError; if (stream) { stream.getTracks().forEach(track => pc.addTrack(track, stream)); } desc = await pc.createOffer(offerOptions); } catch (err) { errDiv.innerText = `Error creating offer: ${err}`; gatherButton.disabled = false; return; } begin = window.performance.now(); candidates = []; pc.setLocalDescription(desc); } // Parse the uint32 PRIORITY field into its constituent parts from RFC 5245, // type preference, local preference, and (256 - component ID). // ex: 126 | 32252 | 255 (126 is host preference, 255 is component ID 1) function formatPriority(priority) { return [ priority >> 24, (priority >> 8) & 0xFFFF, priority & 0xFF ].join(' | '); } function appendCell(row, val) { const cell = document.createElement('td'); cell.textContent = val; row.appendChild(cell); } // Try to determine authentication failures and unreachable TURN // servers by using heuristics on the candidate types gathered. function getFinalResult() { let result = 'Done'; // if more than one server is used, it can not be determined // which server failed. if (servers.length === 1) { const server = JSON.parse(servers[0].value); // get the candidates types (host, srflx, relay) const types = candidates.map((cand) => cand.type); // If the server is a TURN server we should have a relay candidate. // If we did not get a relay candidate but a srflx candidate // authentication might have failed. // If we did not get a relay candidate or a srflx candidate // we could not reach the TURN server. Either it is not running at // the target address or the clients access to the port is blocked. // // This only works for TURN/UDP since we do not get // srflx candidates from TURN/TCP. if (server.urls[0].indexOf('turn:') === 0 && server.urls[0].indexOf('?transport=tcp') === -1) { if (types.indexOf('relay') === -1) { if (types.indexOf('srflx') > -1) { // a binding response but no relay candidate suggests auth failure. result = 'Authentication failed?'; } else { // either the TURN server is down or the clients access is blocked. result = 'Not reachable?'; } } } } return result; } async function iceCallback(event) { const elapsed = ((window.performance.now() - begin) / 1000).toFixed(3); const row = document.createElement('tr'); if (event.candidate) { if (event.candidate.candidate === '') { return; } appendCell(row, elapsed); const {candidate} = event; let url; // Until url is available from the candidate, to to polyfill. if (['srflx', 'relay'].includes(candidate.type) && !candidate.url) { const stats = await pc.getStats(); stats.forEach(report => { if (!url && report.type === 'local-candidate' && report.address === candidate.address && report.port === candidate.port) { url = report.url; } }); } appendCell(row, candidate.type); appendCell(row, candidate.foundation); appendCell(row, candidate.protocol); appendCell(row, candidate.address); appendCell(row, candidate.port); appendCell(row, formatPriority(candidate.priority)); appendCell(row, candidate.url || url || ''); appendCell(row, candidate.relayProtocol || ''); candidates.push(candidate); } candidateTBody.appendChild(row); } function gatheringStateChange() { if (pc.iceGatheringState !== 'complete') { return; } const elapsed = ((window.performance.now() - begin) / 1000).toFixed(3); const row = document.createElement('tr'); appendCell(row, elapsed); appendCell(row, getFinalResult()); pc.close(); pc = null; if (stream) { stream.getTracks().forEach(track => track.stop()); stream = null; } gatherButton.disabled = false; getUserMediaInput.disabled = false; candidateTBody.appendChild(row); } function iceCandidateError(e) { // The interesting attributes of the error are // * the url (which allows looking up the server) // * the errorCode and errorText document.getElementById('error-note').style.display = 'block'; document.getElementById('error').innerText += 'The server ' + e.url + ' returned an error with code=' + e.errorCode + ':\n' + e.errorText + '\n'; } readServersFromLocalStorage(); // check if we have getUserMedia permissions. navigator.mediaDevices .enumerateDevices() .then(function(devices) { devices.forEach(function(device) { if (device.label !== '') { document.getElementById('getUserMediaPermissions').style.display = 'block'; } }); }); ================================================ FILE: src/content/peerconnection/trickle-ice/js/test.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/peerconnection/trickle-ice/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('Trickle-Ice', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); afterEach(() => { return driver.executeScript(() => localStorage.clear()); }); it('gathers a candidate', async () => { await driver.findElement(webdriver.By.id('gather')).click(); await driver.wait(() => driver.executeScript(() => pc === null && candidates.length > 0), 30 * 1000); // eslint-disable-line no-undef }); it('loads server data on double click', async () => { const element = await driver.findElement(webdriver.By.css('#servers>option')); const actions = driver.actions({async: true}); await actions.doubleClick(element).perform(); const value = await driver.findElement(webdriver.By.id('url')).getAttribute('value'); expect(value).not.toBe(''); }); it('adding a server', async () => { await driver.findElement(webdriver.By.id('url')) .sendKeys('stun:stun.l.google.com:19302'); await driver.findElement(webdriver.By.id('add')).click(); const length = await driver.findElement(webdriver.By.css('#servers')) .getAttribute('length'); expect(length >>> 0).toBe(2); }); it('removing a server', async () => { await driver.findElement(webdriver.By.css('#servers>option')).click(); await driver.findElement(webdriver.By.id('remove')).click(); const length = await driver.findElement(webdriver.By.css('#servers')) .getAttribute('length'); expect(length >>> 0).toBe(0); }); }); ================================================ FILE: src/content/peerconnection/upgrade/css/main.css ================================================ /* * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 0 0; width: 83px; } button#hangupButton { margin: 0; } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); margin: 0 0 20px 0; vertical-align: top; } video#localVideo { margin: 0 20px 20px 0; } @media screen and (max-width: 400px) { button { width: 83px; margin: 0 11px 10px 0; } video { height: 90px; margin: 0 0 10px 0; width: calc(50% - 7px); } video#localVideo { margin: 0 10px 20px 0; } } ================================================ FILE: src/content/peerconnection/upgrade/index.html ================================================ Peer connection - upgrade

WebRTC samples Peer connection

View the console to see logging. The MediaStream object localStream, and the RTCPeerConnection objects pc1 and pc2 are in global scope, so you can inspect them in the console as well.

For more information about RTCPeerConnection, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/peerconnection/upgrade/js/main.js ================================================ /* * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); const upgradeButton = document.getElementById('upgradeButton'); const hangupButton = document.getElementById('hangupButton'); callButton.disabled = true; hangupButton.disabled = true; upgradeButton.disabled = true; startButton.onclick = start; callButton.onclick = call; upgradeButton.onclick = upgrade; hangupButton.onclick = hangup; let startTime; const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); localVideo.addEventListener('loadedmetadata', function() { console.log(`Local video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.addEventListener('loadedmetadata', function() { console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.onresize = () => { console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`); console.warn('RESIZE', remoteVideo.videoWidth, remoteVideo.videoHeight); // We'll use the first onsize callback as an indication that video has started // playing out. if (startTime) { const elapsedTime = window.performance.now() - startTime; console.log(`Setup time: ${elapsedTime.toFixed(3)}ms`); startTime = null; } }; let localStream; let pc1; let pc2; const offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 0 }; function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } function gotStream(stream) { console.log('Received local stream'); localVideo.srcObject = stream; localStream = stream; callButton.disabled = false; } function start() { console.log('Requesting local stream'); startButton.disabled = true; navigator.mediaDevices .getUserMedia({ audio: true, video: false }) .then(gotStream) .catch(e => alert(`getUserMedia() error: ${e.name}`)); } function call() { callButton.disabled = true; upgradeButton.disabled = false; hangupButton.disabled = false; console.log('Starting call'); startTime = window.performance.now(); const audioTracks = localStream.getAudioTracks(); if (audioTracks.length > 0) { console.log(`Using audio device: ${audioTracks[0].label}`); } const servers = null; pc1 = new RTCPeerConnection(servers); console.log('Created local peer connection object pc1'); pc1.onicecandidate = e => onIceCandidate(pc1, e); pc2 = new RTCPeerConnection(servers); console.log('Created remote peer connection object pc2'); pc2.onicecandidate = e => onIceCandidate(pc2, e); pc1.oniceconnectionstatechange = e => onIceStateChange(pc1, e); pc2.oniceconnectionstatechange = e => onIceStateChange(pc2, e); pc2.ontrack = gotRemoteStream; localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); console.log('Added local stream to pc1'); console.log('pc1 createOffer start'); pc1.createOffer(offerOptions).then(onCreateOfferSuccess, onCreateSessionDescriptionError); } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); } function onCreateOfferSuccess(desc) { console.log(`Offer from pc1\n${desc.sdp}`); console.log('pc1 setLocalDescription start'); pc1.setLocalDescription(desc).then(() => onSetLocalSuccess(pc1), onSetSessionDescriptionError); console.log('pc2 setRemoteDescription start'); pc2.setRemoteDescription(desc).then(() => onSetRemoteSuccess(pc2), onSetSessionDescriptionError); console.log('pc2 createAnswer start'); // Since the 'remote' side has no media stream we need // to pass in the right constraints in order for it to // accept the incoming offer of audio and video. pc2.createAnswer().then(onCreateAnswerSuccess, onCreateSessionDescriptionError); } function onSetLocalSuccess(pc) { console.log(`${getName(pc)} setLocalDescription complete`); } function onSetRemoteSuccess(pc) { console.log(`${getName(pc)} setRemoteDescription complete`); } function onSetSessionDescriptionError(error) { console.log(`Failed to set session description: ${error.toString()}`); } function gotRemoteStream(e) { console.log('gotRemoteStream', e.track, e.streams[0]); // reset srcObject to work around minor bugs in Chrome and Edge. remoteVideo.srcObject = null; remoteVideo.srcObject = e.streams[0]; } function onCreateAnswerSuccess(desc) { console.log(`Answer from pc2: ${desc.sdp}`); console.log('pc2 setLocalDescription start'); pc2.setLocalDescription(desc).then(() => onSetLocalSuccess(pc2), onSetSessionDescriptionError); console.log('pc1 setRemoteDescription start'); pc1.setRemoteDescription(desc).then(() => onSetRemoteSuccess(pc1), onSetSessionDescriptionError); } function onIceCandidate(pc, event) { getOtherPc(pc) .addIceCandidate(event.candidate) .then(() => onAddIceCandidateSuccess(pc), err => onAddIceCandidateError(pc, err)); console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess(pc) { console.log(`${getName(pc)} addIceCandidate success`); } function onAddIceCandidateError(pc, error) { console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`); } function onIceStateChange(pc, event) { if (pc) { console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`); console.log('ICE state change event: ', event); } } function upgrade() { upgradeButton.disabled = true; navigator.mediaDevices .getUserMedia({video: true}) .then(stream => { const videoTracks = stream.getVideoTracks(); if (videoTracks.length > 0) { console.log(`Using video device: ${videoTracks[0].label}`); } localStream.addTrack(videoTracks[0]); localVideo.srcObject = null; localVideo.srcObject = localStream; pc1.addTrack(videoTracks[0], localStream); return pc1.createOffer(); }) .then(offer => pc1.setLocalDescription(offer)) .then(() => pc2.setRemoteDescription(pc1.localDescription)) .then(() => pc2.createAnswer()) .then(answer => pc2.setLocalDescription(answer)) .then(() => pc1.setRemoteDescription(pc2.localDescription)); } function hangup() { console.log('Ending call'); pc1.close(); pc2.close(); pc1 = null; pc2 = null; const videoTracks = localStream.getVideoTracks(); videoTracks.forEach(videoTrack => { videoTrack.stop(); localStream.removeTrack(videoTrack); }); localVideo.srcObject = null; localVideo.srcObject = localStream; hangupButton.disabled = true; callButton.disabled = false; } ================================================ FILE: src/content/peerconnection/upgrade/js/test.js ================================================ /* * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; const webdriver = require('selenium-webdriver'); const seleniumHelpers = require('../../../../../test/webdriver'); let driver; const path = '/src/content/peerconnection/upgrade/index.html'; const url = `${process.env.BASEURL ? process.env.BASEURL : ('file://' + process.cwd())}${path}`; describe('peerconnection upgrade from audio-only to audio-video', () => { beforeAll(async () => { driver = await seleniumHelpers.buildDriver(); }); afterAll(() => { return driver.quit(); }); beforeEach(() => { return driver.get(url); }); it('upgrades to video', async () => { await driver.findElement(webdriver.By.id('startButton')).click(); await driver.wait(() => driver.executeScript(() => { return localStream !== null; // eslint-disable-line no-undef })); await driver.wait(() => driver.findElement(webdriver.By.id('callButton')).isEnabled()); await driver.findElement(webdriver.By.id('callButton')).click(); await driver.wait(() => driver.executeScript(() => { return pc2 && pc2.connectionState === 'connected'; // eslint-disable-line no-undef })); await driver.wait(() => driver.findElement(webdriver.By.id('upgradeButton')).isEnabled()); await driver.findElement(webdriver.By.id('upgradeButton')).click(); await driver.wait(() => driver.executeScript(() => { return remoteVideo.videoWidth > 0; // eslint-disable-line no-undef })); await driver.wait(() => driver.findElement(webdriver.By.id('hangupButton')).isEnabled()); await driver.findElement(webdriver.By.id('hangupButton')).click(); await driver.wait(() => driver.executeScript(() => { return pc1 === null; // eslint-disable-line no-undef })); }); }); ================================================ FILE: src/content/peerconnection/video-analyzer/index.html ================================================ Page move

The page has moved to: this page

================================================ FILE: src/content/peerconnection/webaudio-input/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ audio { margin: 0 0 20px 0; width: 50%; } button { margin: 0 20px 20px 0; width: 89px; } div#options { margin: 0 0 20px 0; } div#status { background-color: #eee; margin: 0 0 20px 0; min-height: 140px; overflow-y: scroll; padding: 0 0 0 10px; width: 50%; } input[type='checkbox'] { margin: 0 10px 0 0; position: relative; top: -2px; } label { font-weight: 400; } li { margin: 0 0 10px 0; } ul { list-style-type: square; padding: 0 0 0 18px; } ================================================ FILE: src/content/peerconnection/webaudio-input/index.html ================================================ Web Audio input

WebRTC samples Web Audio input

Capture microphone input and stream it to a peer with processing applied to the audio.

The audio stream is:

  • Recorded using live web audio input in chrome://flags .
  • Filtered using an HP filter with fc=1500 Hz.
  • Encoded using Opus.
  • Transmitted (in loopback) to a remote peer using RTCPeerConnection where it is decoded.
  • Finally, the received remote stream is used as source to an <audio> element and played out locally.

Press any key to add an effect to the transmitted audio while talking.

Please note that:

  • Sample rate and channel configuration must be the same for input and output sides on Windows.
  • Only the default microphone device can be used for capturing.

For more information, see WebRTC integration with the Web Audio API.

View source on GitHub
================================================ FILE: src/content/peerconnection/webaudio-input/js/main.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; /* global WebAudioExtended */ const audioElement = document.querySelector('audio'); const startButton = document.querySelector('button#start'); const stopButton = document.querySelector('button#stop'); startButton.onclick = start; stopButton.onclick = stop; const renderLocallyCheckbox = document.querySelector('input#renderLocally'); renderLocallyCheckbox.onclick = toggleRenderLocally; document.addEventListener('keydown', handleKeyDown, false); let localStream; let pc1; let pc2; const webAudio = new WebAudioExtended(); webAudio.loadSound('audio/Shamisen-C4.wav'); function start() { webAudio.start(); const constraints = { audio: true, video: false }; navigator.mediaDevices .getUserMedia(constraints) .then(handleSuccess) .catch(handleFailure); startButton.disabled = true; stopButton.disabled = false; } function stop() { webAudio.stop(); pc1.close(); pc2.close(); pc1 = null; pc2 = null; startButton.enabled = true; stopButton.enabled = false; renderLocallyCheckbox.disabled = true; localStream.getTracks().forEach(track => track.stop()); } function handleSuccess(stream) { renderLocallyCheckbox.disabled = false; const audioTracks = stream.getAudioTracks(); if (audioTracks.length === 1) { console.log('Got one audio track:', audioTracks); const filteredStream = webAudio.applyFilter(stream); const servers = null; pc1 = new RTCPeerConnection(servers); // eslint-disable-line new-cap console.log('Created local peer connection object pc1'); pc1.onicecandidate = e => onIceCandidate(pc1, e); pc2 = new RTCPeerConnection(servers); // eslint-disable-line new-cap console.log('Created remote peer connection object pc2'); pc2.onicecandidate = e => onIceCandidate(pc2, e); pc2.ontrack = gotRemoteStream; filteredStream.getTracks().forEach(track => pc1.addTrack(track, filteredStream)); pc1.createOffer().then(gotDescription1).catch(error => console.log(`createOffer failed: ${error}`)); stream.oninactive = () => { console.log('Stream inactive:', stream); startButton.disabled = false; stopButton.disabled = true; }; localStream = stream; } else { logError('The media stream contains an invalid number of audio tracks.'); stream.getTracks().forEach(track => track.stop()); } } function handleFailure(error) { startButton.disabled = false; stopButton.disabled = true; logError(`Failed to get access to local media. Error: ${error.name}`); } function gotDescription1(desc) { console.log(`Offer from pc1\n${desc.sdp}`); pc1.setLocalDescription(desc); pc2.setRemoteDescription(desc); pc2.createAnswer() .then(gotDescription2) .catch(error => logError(`createAnswer failed: ${error}`)); } function gotDescription2(desc) { console.log(`Answer from pc2\n${desc.sdp}`); pc2.setLocalDescription(desc); pc1.setRemoteDescription(desc); } function gotRemoteStream(e) { if (audioElement.srcObject !== e.streams[0]) { audioElement.srcObject = e.streams[0]; } } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function onIceCandidate(pc, event) { getOtherPc(pc) .addIceCandidate(event.candidate) .then(() => onAddIceCandidateSuccess(pc), err => onAddIceCandidateError(pc, err)); console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess() { console.log('AddIceCandidate success.'); } function onAddIceCandidateError(error) { logError(`Failed to add Ice Candidate: ${error.toString()}`); } function handleKeyDown() { webAudio.addEffect(); } function toggleRenderLocally() { console.log('Render locally: ', renderLocallyCheckbox.checked); webAudio.renderLocally(renderLocallyCheckbox.checked); } function logError(error) { document.querySelector('#errorMsg').innerHTML = error; } ================================================ FILE: src/content/peerconnection/webaudio-input/js/webaudioextended.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; // WebAudioExtended helper class which takes care of the WebAudio related parts. function WebAudioExtended() { window.AudioContext = window.AudioContext || window.webkitAudioContext; /* global AudioContext */ this.context = new AudioContext(); this.soundBuffer = null; } WebAudioExtended.prototype.start = function() { this.filter = this.context.createBiquadFilter(); this.filter.type = 'highpass'; this.filter.frequency.setValueAtTime(1500, this.context.currentTime + 1); }; WebAudioExtended.prototype.applyFilter = function(stream) { this.mic = this.context.createMediaStreamSource(stream); this.mic.connect(this.filter); this.peer = this.context.createMediaStreamDestination(); this.filter.connect(this.peer); return this.peer.stream; }; WebAudioExtended.prototype.renderLocally = function(enabled) { if (enabled) { this.mic.connect(this.context.destination); } else { this.mic.disconnect(0); this.mic.connect(this.filter); } }; WebAudioExtended.prototype.stop = function() { this.mic.disconnect(0); this.filter.disconnect(0); this.mic = null; this.peer = null; }; WebAudioExtended.prototype.addEffect = function() { const effect = this.context.createBufferSource(); effect.buffer = this.soundBuffer; if (this.peer) { effect.connect(this.peer); effect.start(0); } }; WebAudioExtended.prototype.loadCompleted = function() { this.context.decodeAudioData(this.request.response, function(buffer) { this.soundBuffer = buffer; }.bind(this)); }; WebAudioExtended.prototype.loadSound = function(url) { this.request = new XMLHttpRequest(); this.request.open('GET', url, true); this.request.responseType = 'arraybuffer'; this.request.onload = this.loadCompleted.bind(this); this.request.send(); }; ================================================ FILE: src/content/peerconnection/webaudio-output/css/main.css ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 0 0; width: 83px; } button#hangupButton { margin: 0; } canvas { background-color: #666; vertical-align: top; --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); } video { --width: 45%; width: var(--width); height: calc(var(--width) * 0.75); margin: 0 20px 20px 0; } video#remoteVideo { display: none; } @media screen and (max-width: 400px) { button { margin: 0 11px 10px 0; width: 83px; } } ================================================ FILE: src/content/peerconnection/webaudio-output/index.html ================================================ Peer connection as input to Web Audio

WebRTC samples Peer connection as input to Web Audio

View the console to see logging. The MediaStream object localStream, and the RTCPeerConnection objects pc1 and pc2 are in global scope, so you can inspect them in the console as well.

For more information about RTCPeerConnection, see Getting Started With WebRTC.

View source on GitHub
================================================ FILE: src/content/peerconnection/webaudio-output/js/main.js ================================================ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* globals StreamVisualizer */ 'use strict'; const startButton = document.getElementById('startButton'); const callButton = document.getElementById('callButton'); const hangupButton = document.getElementById('hangupButton'); callButton.disabled = true; hangupButton.disabled = true; startButton.onclick = start; callButton.onclick = call; hangupButton.onclick = hangup; const canvas = document.querySelector('canvas'); let startTime; const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); localVideo.addEventListener('loadedmetadata', () => { return console.log(`Local video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.addEventListener('loadedmetadata', () => { return console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); }); remoteVideo.addEventListener('resize', () => { console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`); // We'll use the first onsize callback as an indication that video has started // playing out. if (startTime) { const elapsedTime = window.performance.now() - startTime; console.log(`Setup time: ${elapsedTime.toFixed(3)}ms`); startTime = null; } }); let localStream; let pc1; let pc2; const offerOptions = { offerToReceiveAudio: 1, offerToReceiveVideo: 1 }; function getName(pc) { return (pc === pc1) ? 'pc1' : 'pc2'; } function getOtherPc(pc) { return (pc === pc1) ? pc2 : pc1; } function gotStream(stream) { console.log('Received local stream'); localVideo.srcObject = stream; localStream = stream; callButton.disabled = false; } function start() { console.log('Requesting local stream'); startButton.disabled = true; navigator.mediaDevices .getUserMedia({ audio: true, video: true }) .then(gotStream) .catch(e => alert(`getUserMedia() error: ${e.name}`)); } function call() { callButton.disabled = true; hangupButton.disabled = false; console.log('Starting call'); startTime = window.performance.now(); const videoTracks = localStream.getVideoTracks(); const audioTracks = localStream.getAudioTracks(); if (videoTracks.length > 0) { console.log(`Using video device: ${videoTracks[0].label}`); } if (audioTracks.length > 0) { console.log(`Using audio device: ${audioTracks[0].label}`); } const servers = null; pc1 = new RTCPeerConnection(servers); console.log('Created local peer connection object pc1'); pc1.onicecandidate = e => onIceCandidate(pc1, e); pc2 = new RTCPeerConnection(servers); console.log('Created remote peer connection object pc2'); pc2.onicecandidate = e => onIceCandidate(pc2, e); pc1.oniceconnectionstatechange = e => onIceStateChange(pc1, e); pc2.oniceconnectionstatechange = e => onIceStateChange(pc2, e); pc2.ontrack = gotRemoteStream; localStream.getTracks().forEach(track => pc1.addTrack(track, localStream)); console.log('Added local stream to pc1'); console.log('pc1 createOffer start'); pc1.createOffer(offerOptions).then(onCreateOfferSuccess, onCreateSessionDescriptionError); } function onCreateSessionDescriptionError(error) { console.log(`Failed to create session description: ${error.toString()}`); } function onCreateOfferSuccess(desc) { console.log(`Offer from pc1\n${desc.sdp}`); console.log('pc1 setLocalDescription start'); pc1.setLocalDescription(desc).then(() => onSetLocalSuccess(pc1), onSetSessionDescriptionError); console.log('pc2 setRemoteDescription start'); pc2.setRemoteDescription(desc).then(() => onSetRemoteSuccess(pc2), onSetSessionDescriptionError); console.log('pc2 createAnswer start'); // Since the 'remote' side has no media stream we need // to pass in the right constraints in order for it to // accept the incoming offer of audio and video. pc2.createAnswer().then(onCreateAnswerSuccess, onCreateSessionDescriptionError); } function onSetLocalSuccess(pc) { console.log(`${getName(pc)} setLocalDescription complete`); } function onSetRemoteSuccess(pc) { console.log(`${getName(pc)} setRemoteDescription complete`); } function onSetSessionDescriptionError(error) { console.log(`Failed to set session description: ${error.toString()}`); } function gotRemoteStream(e) { if (remoteVideo.srcObject !== e.streams[0]) { remoteVideo.srcObject = e.streams[0]; console.log('pc2 received remote stream'); const streamVisualizer = new StreamVisualizer(e.streams[0], canvas); streamVisualizer.start(); } } function onCreateAnswerSuccess(desc) { console.log(`Answer from pc2:\n${desc.sdp}`); console.log('pc2 setLocalDescription start'); pc2.setLocalDescription(desc).then(() => onSetLocalSuccess(pc2), onSetSessionDescriptionError); console.log('pc1 setRemoteDescription start'); pc1.setRemoteDescription(desc).then(() => onSetRemoteSuccess(pc1), onSetSessionDescriptionError); } function onIceCandidate(pc, event) { getOtherPc(pc) .addIceCandidate(event.candidate) .then(() => onAddIceCandidateSuccess(pc), err => onAddIceCandidateError(pc, err)); console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); } function onAddIceCandidateSuccess(pc) { console.log(`${getName(pc)} addIceCandidate success`); } function onAddIceCandidateError(pc, error) { console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`); } function onIceStateChange(pc, event) { if (pc) { console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`); console.log('ICE state change event: ', event); } } function hangup() { console.log('Ending call'); pc1.close(); pc2.close(); pc1 = null; pc2 = null; hangupButton.disabled = true; callButton.disabled = false; } ================================================ FILE: src/css/main.css ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ .hidden { display: none; } .highlight { background-color: #eee; font-size: 1.2em; margin: 0 0 30px 0; padding: 0.2em 1.5em; } .warning { color: red; font-weight: 400; } @media screen and (min-width: 1000px) { /* hack! to detect non-touch devices */ div#links a { line-height: 0.8em; } } audio { max-width: 100%; } body { font-family: 'Roboto', sans-serif; font-weight: 300; margin: 0; padding: 1em; word-break: break-word; } button { background-color: #d84a38; border: none; border-radius: 2px; color: white; font-family: 'Roboto', sans-serif; font-size: 0.8em; margin: 0 0 1em 0; padding: 0.5em 0.7em 0.6em 0.7em; } button:active { background-color: #cf402f; } button:hover { background-color: #cf402f; } button[disabled] { color: #ccc; } button[disabled]:hover { background-color: #d84a38; } canvas { background-color: #ccc; max-width: 100%; width: 100%; } code { font-family: 'Roboto', sans-serif; font-weight: 400; } div#container { margin: 0 auto 0 auto; max-width: 60em; padding: 1em 1.5em 1.3em 1.5em; } div#links { padding: 0.5em 0 0 0; } h1 { border-bottom: 1px solid #ccc; font-family: 'Roboto', sans-serif; font-weight: 500; margin: 0 0 0.8em 0; padding: 0 0 0.2em 0; } h2 { color: #444; font-weight: 500; } h3 { border-top: 1px solid #eee; color: #666; font-weight: 500; margin: 10px 0 10px 0; white-space: nowrap; } li { margin: 0 0 0.4em 0; } html { /* avoid annoying page width change when moving from the home page */ overflow-y: scroll; } img { border: none; max-width: 100%; } input[type=radio] { position: relative; top: -1px; } p { color: #444; font-weight: 300; } p#data { border-top: 1px dotted #666; font-family: Courier New, monospace; line-height: 1.3em; max-height: 1000px; overflow-y: auto; padding: 1em 0 0 0; } p.borderBelow { border-bottom: 1px solid #aaa; padding: 0 0 20px 0; } section p:last-of-type { margin: 0; } section { border-bottom: 1px solid #eee; margin: 0 0 30px 0; padding: 0 0 20px 0; } section:last-of-type { border-bottom: none; padding: 0 0 1em 0; } select { margin: 0 1em 1em 0; position: relative; top: -1px; } h1 span { white-space: nowrap; } a { color: #1D6EEE; font-weight: 300; text-decoration: none; } h1 a { font-weight: 300; margin: 0 10px 0 0; white-space: nowrap; } a:hover { color: #3d85c6; text-decoration: underline; } a#viewSource { display: block; margin: 1.3em 0 0 0; border-top: 1px solid #999; padding: 1em 0 0 0; } div#errorMsg p { color: #F00; } div#links a { display: block; line-height: 1.3em; margin: 0 0 1.5em 0; } div.outputSelector { margin: -1.3em 0 2em 0; } p.description { margin: 0 0 0.5em 0; } strong { font-weight: 500; } textarea { resize: none; font-family: 'Roboto', sans-serif; } video { background: #222; margin: 0 0 20px 0; --width: 100%; width: var(--width); height: calc(var(--width) * 0.75); } ul { margin: 0 0 0.5em 0; } fieldset { margin: 0 0 1em 0; } fieldset > select { margin-top: 1em; } @media screen and (max-width: 650px) { .highlight { font-size: 1em; margin: 0 0 20px 0; padding: 0.2em 1em; } h1 { font-size: 24px; } } @media screen and (max-width: 550px) { button:active { background-color: darkRed; } h1 { font-size: 22px; } } @media screen and (max-width: 450px) { h1 { font-size: 20px; } } ================================================ FILE: src/js/lib/ga.js ================================================ (function(i, s, o, g, r, a, m) { i['GoogleAnalyticsObject']=r; i[r]=i[r]||function() { (i[r].q=i[r].q||[]).push(arguments); }, i[r].l=1*new Date(); a=s.createElement(o), m=s.getElementsByTagName(o)[0]; a.async=1; a.src=g; m.parentNode.insertBefore(a, m); })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); ga('create', 'UA-48530561-1', 'auto'); ga('send', 'pageview'); ================================================ FILE: src/js/third_party/graph.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ // taken from chrome://webrtc-internals with jshint adaptions 'use strict'; /* exported TimelineDataSeries, TimelineGraphView */ // The maximum number of data points bufferred for each stats. Old data points // will be shifted out when the buffer is full. const MAX_STATS_DATA_POINT_BUFFER_SIZE = 1000; const TimelineDataSeries = (function() { /** * @constructor */ function TimelineDataSeries() { // List of DataPoints in chronological order. this.dataPoints_ = []; // Default color. Should always be overridden prior to display. this.color_ = 'red'; // Whether or not the data series should be drawn. this.isVisible_ = true; this.cacheStartTime_ = null; this.cacheStepSize_ = 0; this.cacheValues_ = []; } TimelineDataSeries.prototype = { /** * @override */ toJSON: function() { if (this.dataPoints_.length < 1) { return {}; } let values = []; for (let i = 0; i < this.dataPoints_.length; ++i) { values.push(this.dataPoints_[i].value); } return { startTime: this.dataPoints_[0].time, endTime: this.dataPoints_[this.dataPoints_.length - 1].time, values: JSON.stringify(values), }; }, /** * Adds a DataPoint to |this| with the specified time and value. * DataPoints are assumed to be received in chronological order. */ addPoint: function(timeTicks, value) { let time = new Date(timeTicks); this.dataPoints_.push(new DataPoint(time, value)); if (this.dataPoints_.length > MAX_STATS_DATA_POINT_BUFFER_SIZE) { this.dataPoints_.shift(); } }, isVisible: function() { return this.isVisible_; }, show: function(isVisible) { this.isVisible_ = isVisible; }, getColor: function() { return this.color_; }, setColor: function(color) { this.color_ = color; }, getCount: function() { return this.dataPoints_.length; }, /** * Returns a list containing the values of the data series at |count| * points, starting at |startTime|, and |stepSize| milliseconds apart. * Caches values, so showing/hiding individual data series is fast. */ getValues: function(startTime, stepSize, count) { // Use cached values, if we can. if (this.cacheStartTime_ === startTime && this.cacheStepSize_ === stepSize && this.cacheValues_.length === count) { return this.cacheValues_; } // Do all the work. this.cacheValues_ = this.getValuesInternal_(startTime, stepSize, count); this.cacheStartTime_ = startTime; this.cacheStepSize_ = stepSize; return this.cacheValues_; }, /** * Returns the cached |values| in the specified time period. */ getValuesInternal_: function(startTime, stepSize, count) { let values = []; let nextPoint = 0; let currentValue = 0; let time = startTime; for (let i = 0; i < count; ++i) { while (nextPoint < this.dataPoints_.length && this.dataPoints_[nextPoint].time < time) { currentValue = this.dataPoints_[nextPoint].value; ++nextPoint; } values[i] = currentValue; time += stepSize; } return values; } }; /** * A single point in a data series. Each point has a time, in the form of * milliseconds since the Unix epoch, and a numeric value. * @constructor */ function DataPoint(time, value) { this.time = time; this.value = value; } return TimelineDataSeries; })(); const TimelineGraphView = (function() { // Maximum number of labels placed vertically along the sides of the graph. let MAX_VERTICAL_LABELS = 6; // Vertical spacing between labels and between the graph and labels. let LABEL_VERTICAL_SPACING = 4; // Horizontal spacing between vertically placed labels and the edges of the // graph. let LABEL_HORIZONTAL_SPACING = 3; // Horizintal spacing between two horitonally placed labels along the bottom // of the graph. // var LABEL_LABEL_HORIZONTAL_SPACING = 25; // Length of ticks, in pixels, next to y-axis labels. The x-axis only has // one set of labels, so it can use lines instead. let Y_AXIS_TICK_LENGTH = 10; let GRID_COLOR = '#CCC'; let TEXT_COLOR = '#000'; let BACKGROUND_COLOR = '#FFF'; let MAX_DECIMAL_PRECISION = 3; /** * @constructor */ function TimelineGraphView(divId, canvasId) { this.scrollbar_ = {position_: 0, range_: 0}; this.graphDiv_ = document.getElementById(divId); this.canvas_ = document.getElementById(canvasId); // Set the range and scale of the graph. Times are in milliseconds since // the Unix epoch. // All measurements we have must be after this time. this.startTime_ = 0; // The current rightmost position of the graph is always at most this. this.endTime_ = 1; this.graph_ = null; // Horizontal scale factor, in terms of milliseconds per pixel. this.scale_ = 1000; // Initialize the scrollbar. this.updateScrollbarRange_(true); } TimelineGraphView.prototype = { setScale: function(scale) { this.scale_ = scale; }, // Returns the total length of the graph, in pixels. getLength_: function() { let timeRange = this.endTime_ - this.startTime_; // Math.floor is used to ignore the last partial area, of length less // than this.scale_. return Math.floor(timeRange / this.scale_); }, /** * Returns true if the graph is scrolled all the way to the right. */ graphScrolledToRightEdge_: function() { return this.scrollbar_.position_ === this.scrollbar_.range_; }, /** * Update the range of the scrollbar. If |resetPosition| is true, also * sets the slider to point at the rightmost position and triggers a * repaint. */ updateScrollbarRange_: function(resetPosition) { let scrollbarRange = this.getLength_() - this.canvas_.width; if (scrollbarRange < 0) { scrollbarRange = 0; } // If we've decreased the range to less than the current scroll position, // we need to move the scroll position. if (this.scrollbar_.position_ > scrollbarRange) { resetPosition = true; } this.scrollbar_.range_ = scrollbarRange; if (resetPosition) { this.scrollbar_.position_ = scrollbarRange; this.repaint(); } }, /** * Sets the date range displayed on the graph, switches to the default * scale factor, and moves the scrollbar all the way to the right. */ setDateRange: function(startDate, endDate) { this.startTime_ = startDate.getTime(); this.endTime_ = endDate.getTime(); // Safety check. if (this.endTime_ <= this.startTime_) { this.startTime_ = this.endTime_ - 1; } this.updateScrollbarRange_(true); }, /** * Updates the end time at the right of the graph to be the current time. * Specifically, updates the scrollbar's range, and if the scrollbar is * all the way to the right, keeps it all the way to the right. Otherwise, * leaves the view as-is and doesn't redraw anything. */ updateEndDate: function(optDate) { this.endTime_ = optDate || (new Date()).getTime(); this.updateScrollbarRange_(this.graphScrolledToRightEdge_()); }, getStartDate: function() { return new Date(this.startTime_); }, /** * Replaces the current TimelineDataSeries with |dataSeries|. */ setDataSeries: function(dataSeries) { // Simply recreates the Graph. this.graph_ = new Graph(); for (let i = 0; i < dataSeries.length; ++i) { this.graph_.addDataSeries(dataSeries[i]); } this.repaint(); }, /** * Adds |dataSeries| to the current graph. */ addDataSeries: function(dataSeries) { if (!this.graph_) { this.graph_ = new Graph(); } this.graph_.addDataSeries(dataSeries); this.repaint(); }, /** * Draws the graph on |canvas_|. */ repaint: function() { this.repaintTimerRunning_ = false; let width = this.canvas_.width; let height = this.canvas_.height; let context = this.canvas_.getContext('2d'); // Clear the canvas. context.fillStyle = BACKGROUND_COLOR; context.fillRect(0, 0, width, height); // Try to get font height in pixels. Needed for layout. let fontHeightString = context.font.match(/([0-9]+)px/)[1]; let fontHeight = parseInt(fontHeightString); // Safety check, to avoid drawing anything too ugly. if (fontHeightString.length === 0 || fontHeight <= 0 || fontHeight * 4 > height || width < 50) { return; } // Save current transformation matrix so we can restore it later. context.save(); // The center of an HTML canvas pixel is technically at (0.5, 0.5). This // makes near straight lines look bad, due to anti-aliasing. This // translation reduces the problem a little. context.translate(0.5, 0.5); // Figure out what time values to display. let position = this.scrollbar_.position_; // If the entire time range is being displayed, align the right edge of // the graph to the end of the time range. if (this.scrollbar_.range_ === 0) { position = this.getLength_() - this.canvas_.width; } let visibleStartTime = this.startTime_ + position * this.scale_; // Make space at the bottom of the graph for the time labels, and then // draw the labels. let textHeight = height; height -= fontHeight + LABEL_VERTICAL_SPACING; this.drawTimeLabels(context, width, height, textHeight, visibleStartTime); // Draw outline of the main graph area. context.strokeStyle = GRID_COLOR; context.strokeRect(0, 0, width - 1, height - 1); if (this.graph_) { // Layout graph and have them draw their tick marks. this.graph_.layout( width, height, fontHeight, visibleStartTime, this.scale_); this.graph_.drawTicks(context); // Draw the lines of all graphs, and then draw their labels. this.graph_.drawLines(context); this.graph_.drawLabels(context); } // Restore original transformation matrix. context.restore(); }, /** * Draw time labels below the graph. Takes in start time as an argument * since it may not be |startTime_|, when we're displaying the entire * time range. */ drawTimeLabels: function(context, width, height, textHeight, startTime) { // Draw the labels 1 minute apart. let timeStep = 1000 * 60; // Find the time for the first label. This time is a perfect multiple of // timeStep because of how UTC times work. let time = Math.ceil(startTime / timeStep) * timeStep; context.textBaseline = 'bottom'; context.textAlign = 'center'; context.fillStyle = TEXT_COLOR; context.strokeStyle = GRID_COLOR; // Draw labels and vertical grid lines. while (true) { let x = Math.round((time - startTime) / this.scale_); if (x >= width) { break; } let text = (new Date(time)).toLocaleTimeString(); context.fillText(text, x, textHeight); context.beginPath(); context.lineTo(x, 0); context.lineTo(x, height); context.stroke(); time += timeStep; } }, getDataSeriesCount: function() { if (this.graph_) { return this.graph_.dataSeries_.length; } return 0; }, hasDataSeries: function(dataSeries) { if (this.graph_) { return this.graph_.hasDataSeries(dataSeries); } return false; }, }; /** * A Graph is responsible for drawing all the TimelineDataSeries that have * the same data type. Graphs are responsible for scaling the values, laying * out labels, and drawing both labels and lines for its data series. */ const Graph = (function() { /** * @constructor */ function Graph() { this.dataSeries_ = []; // Cached properties of the graph, set in layout. this.width_ = 0; this.height_ = 0; this.fontHeight_ = 0; this.startTime_ = 0; this.scale_ = 0; // The lowest/highest values adjusted by the vertical label step size // in the displayed range of the graph. Used for scaling and setting // labels. Set in layoutLabels. this.min_ = 0; this.max_ = 0; // Cached text of equally spaced labels. Set in layoutLabels. this.labels_ = []; } /** * A Label is the label at a particular position along the y-axis. * @constructor */ /* function Label(height, text) { this.height = height; this.text = text; } */ Graph.prototype = { addDataSeries: function(dataSeries) { this.dataSeries_.push(dataSeries); }, hasDataSeries: function(dataSeries) { for (let i = 0; i < this.dataSeries_.length; ++i) { if (this.dataSeries_[i] === dataSeries) { return true; } } return false; }, /** * Returns a list of all the values that should be displayed for a given * data series, using the current graph layout. */ getValues: function(dataSeries) { if (!dataSeries.isVisible()) { return null; } return dataSeries.getValues(this.startTime_, this.scale_, this.width_); }, /** * Updates the graph's layout. In particular, both the max value and * label positions are updated. Must be called before calling any of the * drawing functions. */ layout: function(width, height, fontHeight, startTime, scale) { this.width_ = width; this.height_ = height; this.fontHeight_ = fontHeight; this.startTime_ = startTime; this.scale_ = scale; // Find largest value. let max = 0; let min = 0; for (let i = 0; i < this.dataSeries_.length; ++i) { let values = this.getValues(this.dataSeries_[i]); if (!values) { continue; } for (let j = 0; j < values.length; ++j) { if (values[j] > max) { max = values[j]; } else if (values[j] < min) { min = values[j]; } } } this.layoutLabels_(min, max); }, /** * Lays out labels and sets |max_|/|min_|, taking the time units into * consideration. |maxValue| is the actual maximum value, and * |max_| will be set to the value of the largest label, which * will be at least |maxValue|. Similar for |min_|. */ layoutLabels_: function(minValue, maxValue) { if (maxValue - minValue < 1024) { this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION); return; } // Find appropriate units to use. let units = ['', 'k', 'M', 'G', 'T', 'P']; // Units to use for labels. 0 is '1', 1 is K, etc. // We start with 1, and work our way up. let unit = 1; minValue /= 1024; maxValue /= 1024; while (units[unit + 1] && maxValue - minValue >= 1024) { minValue /= 1024; maxValue /= 1024; ++unit; } // Calculate labels. this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION); // Append units to labels. for (let i = 0; i < this.labels_.length; ++i) { this.labels_[i] += ' ' + units[unit]; } // Convert |min_|/|max_| back to unit '1'. this.min_ *= Math.pow(1024, unit); this.max_ *= Math.pow(1024, unit); }, /** * Same as layoutLabels_, but ignores units. |maxDecimalDigits| is the * maximum number of decimal digits allowed. The minimum allowed * difference between two adjacent labels is 10^-|maxDecimalDigits|. */ layoutLabelsBasic_: function(minValue, maxValue, maxDecimalDigits) { this.labels_ = []; let range = maxValue - minValue; // No labels if the range is 0. if (range === 0) { this.min_ = this.max_ = maxValue; return; } // The maximum number of equally spaced labels allowed. |fontHeight_| // is doubled because the top two labels are both drawn in the same // gap. let minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING; // The + 1 is for the top label. let maxLabels = 1 + this.height_ / minLabelSpacing; if (maxLabels < 2) { maxLabels = 2; } else if (maxLabels > MAX_VERTICAL_LABELS) { maxLabels = MAX_VERTICAL_LABELS; } // Initial try for step size between conecutive labels. let stepSize = Math.pow(10, -maxDecimalDigits); // Number of digits to the right of the decimal of |stepSize|. // Used for formating label strings. let stepSizeDecimalDigits = maxDecimalDigits; // Pick a reasonable step size. while (true) { // If we use a step size of |stepSize| between labels, we'll need: // // Math.ceil(range / stepSize) + 1 // // labels. The + 1 is because we need labels at both at 0 and at // the top of the graph. // Check if we can use steps of size |stepSize|. if (Math.ceil(range / stepSize) + 1 <= maxLabels) { break; } // Check |stepSize| * 2. if (Math.ceil(range / (stepSize * 2)) + 1 <= maxLabels) { stepSize *= 2; break; } // Check |stepSize| * 5. if (Math.ceil(range / (stepSize * 5)) + 1 <= maxLabels) { stepSize *= 5; break; } stepSize *= 10; if (stepSizeDecimalDigits > 0) { --stepSizeDecimalDigits; } } // Set the min/max so it's an exact multiple of the chosen step size. this.max_ = Math.ceil(maxValue / stepSize) * stepSize; this.min_ = Math.floor(minValue / stepSize) * stepSize; // Create labels. for (let label = this.max_; label >= this.min_; label -= stepSize) { this.labels_.push(label.toFixed(stepSizeDecimalDigits)); } }, /** * Draws tick marks for each of the labels in |labels_|. */ drawTicks: function(context) { let x1; let x2; x1 = this.width_ - 1; x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH; context.fillStyle = GRID_COLOR; context.beginPath(); for (let i = 1; i < this.labels_.length - 1; ++i) { // The rounding is needed to avoid ugly 2-pixel wide anti-aliased // lines. let y = Math.round(this.height_ * i / (this.labels_.length - 1)); context.moveTo(x1, y); context.lineTo(x2, y); } context.stroke(); }, /** * Draws a graph line for each of the data series. */ drawLines: function(context) { // Factor by which to scale all values to convert them to a number from // 0 to height - 1. let scale = 0; let bottom = this.height_ - 1; if (this.max_) { scale = bottom / (this.max_ - this.min_); } // Draw in reverse order, so earlier data series are drawn on top of // subsequent ones. for (let i = this.dataSeries_.length - 1; i >= 0; --i) { let values = this.getValues(this.dataSeries_[i]); if (!values) { continue; } context.strokeStyle = this.dataSeries_[i].getColor(); context.beginPath(); for (let x = 0; x < values.length; ++x) { // The rounding is needed to avoid ugly 2-pixel wide anti-aliased // horizontal lines. context.lineTo( x, bottom - Math.round((values[x] - this.min_) * scale)); } context.stroke(); } }, /** * Draw labels in |labels_|. */ drawLabels: function(context) { if (this.labels_.length === 0) { return; } let x = this.width_ - LABEL_HORIZONTAL_SPACING; // Set up the context. context.fillStyle = TEXT_COLOR; context.textAlign = 'right'; // Draw top label, which is the only one that appears below its tick // mark. context.textBaseline = 'top'; context.fillText(this.labels_[0], x, 0); // Draw all the other labels. context.textBaseline = 'bottom'; let step = (this.height_ - 1) / (this.labels_.length - 1); for (let i = 1; i < this.labels_.length; ++i) { context.fillText(this.labels_[i], x, step * i); } } }; return Graph; })(); return TimelineGraphView; })(); ================================================ FILE: src/js/third_party/streamvisualizer.js ================================================ /* * Copyright 2016 Boris Smus. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Adapted from Boris Smus's demo at http://webaudioapi.com/samples/visualizer /* globals AudioContext, webkitAudioContext */ const WIDTH = 308; const HEIGHT = 231; // Interesting parameters to tweak! const SMOOTHING = 0.8; const FFT_SIZE = 2048; function StreamVisualizer(remoteStream, canvas) { console.log('Creating StreamVisualizer with remoteStream and canvas: ', remoteStream, canvas); this.canvas = canvas; this.drawContext = this.canvas.getContext('2d'); // cope with browser differences if (typeof AudioContext === 'function') { this.context = new AudioContext(); } else if (typeof webkitAudioContext === 'function') { this.context = new webkitAudioContext(); // eslint-disable-line new-cap } else { alert('Sorry! Web Audio is not supported by this browser'); } // Create a MediaStreamAudioSourceNode from the remoteStream this.source = this.context.createMediaStreamSource(remoteStream); console.log('Created Web Audio source from remote stream: ', this.source); this.analyser = this.context.createAnalyser(); // this.analyser.connect(this.context.destination); this.analyser.minDecibels = -140; this.analyser.maxDecibels = 0; this.freqs = new Uint8Array(this.analyser.frequencyBinCount); this.times = new Uint8Array(this.analyser.frequencyBinCount); this.source.connect(this.analyser); this.startTime = 0; this.startOffset = 0; } StreamVisualizer.prototype.start = function() { requestAnimationFrame(this.draw.bind(this)); }; StreamVisualizer.prototype.draw = function() { let barWidth; let offset; let height; let percent; let value; this.analyser.smoothingTimeConstant = SMOOTHING; this.analyser.fftSize = FFT_SIZE; // Get the frequency data from the currently playing music this.analyser.getByteFrequencyData(this.freqs); this.analyser.getByteTimeDomainData(this.times); this.canvas.width = WIDTH; this.canvas.height = HEIGHT; // Draw the frequency domain chart. for (let i = 0; i < this.analyser.frequencyBinCount; i++) { value = this.freqs[i]; percent = value / 256; height = HEIGHT * percent; offset = HEIGHT - height - 1; barWidth = WIDTH / this.analyser.frequencyBinCount; let hue = i/this.analyser.frequencyBinCount * 360; this.drawContext.fillStyle = 'hsl(' + hue + ', 100%, 50%)'; this.drawContext.fillRect(i * barWidth, offset, barWidth, height); } // Draw the time domain chart. for (let i = 0; i < this.analyser.frequencyBinCount; i++) { value = this.times[i]; percent = value / 256; height = HEIGHT * percent; offset = HEIGHT - height - 1; barWidth = WIDTH/this.analyser.frequencyBinCount; this.drawContext.fillStyle = 'white'; this.drawContext.fillRect(i * barWidth, offset, 1, 2); } requestAnimationFrame(this.draw.bind(this)); }; StreamVisualizer.prototype.getFrequencyValue = function(freq) { let nyquist = this.context.sampleRate/2; let index = Math.round(freq/nyquist * this.freqs.length); return this.freqs[index]; }; ================================================ FILE: src/js/third_party/webgl_teapot/cameracontroller.js ================================================ /* * Copyright (c) 2009 The Chromium Authors. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // A simple camera controller which uses an HTML element as the event // source for constructing a view matrix. Assign an "onchange" // function to the controller as follows to receive the updated X and // Y angles for the camera: // // var controller = new CameraController(canvas); // controller.onchange = function(xRot, yRot) { ... }; // // The view matrix is computed elsewhere. // // opt_canvas (an HTMLCanvasElement) and opt_context (a // WebGLRenderingContext) can be passed in to make the hit detection // more precise -- only opaque pixels will be considered as the start // of a drag action. function CameraController(element, opt_canvas, opt_context) { var controller = this; this.onchange = null; this.xRot = 0; this.yRot = 0; this.scaleFactor = 3.0; this.dragging = false; this.curX = 0; this.curY = 0; if (opt_canvas) this.canvas_ = opt_canvas; if (opt_context) this.context_ = opt_context; function mouseDown(ev) { controller.curX = ev.clientX; controller.curY = ev.clientY; var dragging = false; if (controller.canvas_ && controller.context_) { var rect = controller.canvas_.getBoundingClientRect(); // Transform the event's x and y coordinates into the coordinate // space of the canvas var canvasRelativeX = ev.pageX - rect.left; var canvasRelativeY = ev.pageY - rect.top; var canvasWidth = controller.canvas_.width; var canvasHeight = controller.canvas_.height; // Read back a small portion of the frame buffer around this point if (canvasRelativeX > 0 && canvasRelativeX < canvasWidth && canvasRelativeY > 0 && canvasRelativeY < canvasHeight) { var pixels = new Uint8Array(1); controller.context_.readPixels(canvasRelativeX, canvasHeight - canvasRelativeY, 1, 1, controller.context_.RGBA, controller.context_.UNSIGNED_BYTE, pixels); // See whether this pixel has an alpha value of >= about 10% if (pixels[3] > (255.0 / 10.0)) { dragging = true; } } } else { dragging = true; } controller.dragging = dragging; } function mouseMove(ev) { if (controller.dragging) { // Determine how far we have moved since the last mouse move // event. var curX = ev.clientX; var curY = ev.clientY; var deltaX = (controller.curX - curX) / controller.scaleFactor; var deltaY = (controller.curY - curY) / controller.scaleFactor; controller.curX = curX; controller.curY = curY; // Update the X and Y rotation angles based on the mouse motion. controller.yRot = (controller.yRot + deltaX) % 360; controller.xRot = (controller.xRot + deltaY); // Clamp the X rotation to prevent the camera from going upside // down. if (controller.xRot < -90) { controller.xRot = -90; } else if (controller.xRot > 90) { controller.xRot = 90; } // Send the onchange event to any listener. if (controller.onchange != null) { controller.onchange(controller.xRot, controller.yRot); } } } function mouseUp(ev) { controller.dragging = false; } element.addEventListener("mousedown", mouseDown, false); element.addEventListener("mousemove", mouseMove, false); element.addEventListener("mouseup", mouseUp, false); var activeTouchIdentifier; function findActiveTouch(touches) { for (var ii = 0; ii < touches.length; ++ii) { if (touches.item(ii).identifier == activeTouchIdentifier) { return touches.item(ii); } } return null; } function touchStart(ev) { if (controller.dragging || ev.targetTouches.length == 0) { return; } var touch = ev.targetTouches.item(0); mouseDown(touch); if (controller.dragging) { activeTouchIdentifier = touch.identifier; } ev.preventDefault(); } function touchMove(ev) { if (!controller.dragging) { return; } var touch = findActiveTouch(ev.changedTouches); if (touch) { mouseMove(touch); } ev.preventDefault(); } function touchEnd(ev) { var touch = findActiveTouch(ev.changedTouches); if (touch) { mouseUp(touch); } ev.preventDefault(); } element.addEventListener("touchstart", touchStart, false); element.addEventListener("touchmove", touchMove, false); element.addEventListener("touchend", touchEnd, false); element.addEventListener("touchcancel", touchEnd, false); } ================================================ FILE: src/js/third_party/webgl_teapot/demo.js ================================================ /* * Copyright (c) 2009 The Chromium Authors. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var gl = null; var g_width = 0; var g_height = 0; var g_bumpTexture = null; var g_envTexture = null; var g_programObject = null; var g_vbo = null; var g_elementVbo = null; var g_normalsOffset = 0; var g_tangentsOffset = 0; var g_binormalsOffset = 0; var g_texCoordsOffset = 0; var g_numElements = 0; // Uniform variables var g_worldLoc = 0; var g_worldInverseTransposeLoc = 0; var g_worldViewProjLoc = 0; var g_viewInverseLoc = 0; var g_normalSamplerLoc = 0; var g_envSamplerLoc = 0; var g_pendingTextureLoads = 0; // The "model" matrix is the "world" matrix in Standard Annotations // and Semantics var model = new Matrix4x4(); var view = new Matrix4x4(); var projection = new Matrix4x4(); var controller = null; function main() { var c = document.querySelector("canvas"); //c = WebGLDebugUtils.makeLostContextSimulatingCanvas(c); // tell the simulator when to lose context. //c.loseContextInNCalls(15); c.addEventListener('webglcontextlost', handleContextLost, false); c.addEventListener('webglcontextrestored', handleContextRestored, false); var ratio = window.devicePixelRatio ? window.devicePixelRatio : 1; // original is 480 x 270 c.width = 240 * ratio; c.height = 180 * ratio; gl = WebGLUtils.setupWebGL(c); if (!gl) return; g_width = c.width; g_height = c.height; controller = new CameraController(c); // Try the following (and uncomment the "pointer-events: none;" in // the index.html) to try the more precise hit detection // controller = new CameraController(document.getElementById("body"), c, gl); controller.onchange = function(xRot, yRot) { draw(); }; init(); } function log(msg) { if (window.console && window.console.log) { console.log(msg); } } function handleContextLost(e) { log("handle context lost"); e.preventDefault(); clearLoadingImages(); } function handleContextRestored() { log("handle context restored"); init(); } function output(str) { document.body.appendChild(document.createTextNode(str)); document.body.appendChild(document.createElement("br")); } function checkGLError() { var error = gl.getError(); if (error != gl.NO_ERROR && error != gl.CONTEXT_LOST_WEBGL) { var str = "GL Error: " + error; output(str); throw str; } } function init() { gl.enable(gl.DEPTH_TEST); // Can use this to make the background opaque // gl.clearColor(0.3, 0.2, 0.2, 1.); gl.clearColor(0.0, 0.0, 0.0, 0.0); initTeapot(); initShaders(); g_bumpTexture = loadTexture("../../../js/third_party/webgl_teapot/images/bump.jpg"); g_envTexture = loadCubeMap("../../../js/third_party/webgl_teapot/images/skybox", "jpg"); draw(); } function initTeapot() { g_vbo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, g_vbo); gl.bufferData(gl.ARRAY_BUFFER, teapotPositions.byteLength + teapotNormals.byteLength + teapotTangents.byteLength + teapotBinormals.byteLength + teapotTexCoords.byteLength, gl.STATIC_DRAW); g_normalsOffset = teapotPositions.byteLength; g_tangentsOffset = g_normalsOffset + teapotNormals.byteLength; g_binormalsOffset = g_tangentsOffset + teapotTangents.byteLength; g_texCoordsOffset = g_binormalsOffset + teapotBinormals.byteLength; gl.bufferSubData(gl.ARRAY_BUFFER, 0, teapotPositions); gl.bufferSubData(gl.ARRAY_BUFFER, g_normalsOffset, teapotNormals); gl.bufferSubData(gl.ARRAY_BUFFER, g_tangentsOffset, teapotTangents); gl.bufferSubData(gl.ARRAY_BUFFER, g_binormalsOffset, teapotBinormals); gl.bufferSubData(gl.ARRAY_BUFFER, g_texCoordsOffset, teapotTexCoords); g_elementVbo = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, g_elementVbo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, teapotIndices, gl.STATIC_DRAW); g_numElements = teapotIndices.length; } var bumpReflectVertexSource = [ "attribute vec3 g_Position;", "attribute vec3 g_TexCoord0;", "attribute vec3 g_Tangent;", "attribute vec3 g_Binormal;", "attribute vec3 g_Normal;", "", "uniform mat4 world;", "uniform mat4 worldInverseTranspose;", "uniform mat4 worldViewProj;", "uniform mat4 viewInverse;", "", "varying vec2 texCoord;", "varying vec3 worldEyeVec;", "varying vec3 worldNormal;", "varying vec3 worldTangent;", "varying vec3 worldBinorm;", "", "void main() {", " gl_Position = worldViewProj * vec4(g_Position.xyz, 1.);", " texCoord.xy = g_TexCoord0.xy;", " worldNormal = (worldInverseTranspose * vec4(g_Normal, 1.)).xyz;", " worldTangent = (worldInverseTranspose * vec4(g_Tangent, 1.)).xyz;", " worldBinorm = (worldInverseTranspose * vec4(g_Binormal, 1.)).xyz;", " vec3 worldPos = (world * vec4(g_Position, 1.)).xyz;", " worldEyeVec = normalize(worldPos - viewInverse[3].xyz);", "}" ].join("\n"); var bumpReflectFragmentSource = [ "precision mediump float;\n", "const float bumpHeight = 0.2;", "", "uniform sampler2D normalSampler;", "uniform samplerCube envSampler;", "", "varying vec2 texCoord;", "varying vec3 worldEyeVec;", "varying vec3 worldNormal;", "varying vec3 worldTangent;", "varying vec3 worldBinorm;", "", "void main() {", " vec2 bump = (texture2D(normalSampler, texCoord.xy).xy * 2.0 - 1.0) * bumpHeight;", " vec3 normal = normalize(worldNormal);", " vec3 tangent = normalize(worldTangent);", " vec3 binormal = normalize(worldBinorm);", " vec3 nb = normal + bump.x * tangent + bump.y * binormal;", " nb = normalize(nb);", " vec3 worldEye = normalize(worldEyeVec);", " vec3 lookup = reflect(worldEye, nb);", " vec4 color = textureCube(envSampler, lookup);", " gl_FragColor = color;", "}" ].join("\n"); function loadShader(type, shaderSrc) { var shader = gl.createShader(type); // Load the shader source gl.shaderSource(shader, shaderSrc); // Compile the shader gl.compileShader(shader); // Check the compile status if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS) && !gl.isContextLost()) { var infoLog = gl.getShaderInfoLog(shader); output("Error compiling shader:\n" + infoLog); gl.deleteShader(shader); return null; } return shader; } function initShaders() { var vertexShader = loadShader(gl.VERTEX_SHADER, bumpReflectVertexSource); var fragmentShader = loadShader(gl.FRAGMENT_SHADER, bumpReflectFragmentSource); // Create the program object var programObject = gl.createProgram(); gl.attachShader(programObject, vertexShader); gl.attachShader(programObject, fragmentShader); // Bind attributes gl.bindAttribLocation(programObject, 0, "g_Position"); gl.bindAttribLocation(programObject, 1, "g_TexCoord0"); gl.bindAttribLocation(programObject, 2, "g_Tangent"); gl.bindAttribLocation(programObject, 3, "g_Binormal"); gl.bindAttribLocation(programObject, 4, "g_Normal"); // Link the program gl.linkProgram(programObject); // Check the link status var linked = gl.getProgramParameter(programObject, gl.LINK_STATUS); if (!linked && !gl.isContextLost()) { var infoLog = gl.getProgramInfoLog(programObject); output("Error linking program:\n" + infoLog); gl.deleteProgram(programObject); return; } g_programObject = programObject; // Look up uniform locations g_worldLoc = gl.getUniformLocation(g_programObject, "world"); g_worldInverseTransposeLoc = gl.getUniformLocation(g_programObject, "worldInverseTranspose"); g_worldViewProjLoc = gl.getUniformLocation(g_programObject, "worldViewProj"); g_viewInverseLoc = gl.getUniformLocation(g_programObject, "viewInverse"); g_normalSamplerLoc = gl.getUniformLocation(g_programObject, "normalSampler"); g_envSamplerLoc = gl.getUniformLocation(g_programObject, "envSampler"); checkGLError(); } function draw() { // Note: the viewport is automatically set up to cover the entire Canvas. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); checkGLError(); // For now, don't render if we have incomplete textures, just to // avoid accidentally incurring OpenGL errors -- although we should // be fully able to load textures in in the background if (g_pendingTextureLoads > 0) { return; } // Set up the model, view and projection matrices projection.loadIdentity(); projection.perspective(45, g_width / g_height, 10, 500); view.loadIdentity(); view.translate(0, -10, -100.0); // Add in camera controller's rotation model.loadIdentity(); model.rotate(controller.xRot, 1, 0, 0); model.rotate(controller.yRot, 0, 1, 0); // Correct for initial placement and orientation of model model.translate(0, -10, 0); model.rotate(90, 1, 0, 0); gl.useProgram(g_programObject); // Compute necessary matrices var mvp = new Matrix4x4(); mvp.multiply(model); mvp.multiply(view); mvp.multiply(projection); var worldInverseTranspose = model.inverse(); worldInverseTranspose.transpose(); var viewInverse = view.inverse(); // Set up uniforms gl.uniformMatrix4fv(g_worldLoc, gl.FALSE, new Float32Array(model.elements)); gl.uniformMatrix4fv(g_worldInverseTransposeLoc, gl.FALSE, new Float32Array(worldInverseTranspose.elements)); gl.uniformMatrix4fv(g_worldViewProjLoc, gl.FALSE, new Float32Array(mvp.elements)); gl.uniformMatrix4fv(g_viewInverseLoc, gl.FALSE, new Float32Array(viewInverse.elements)); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, g_bumpTexture); gl.uniform1i(g_normalSamplerLoc, 0); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_CUBE_MAP, g_envTexture); gl.uniform1i(g_envSamplerLoc, 1); checkGLError(); // Bind and set up vertex streams gl.bindBuffer(gl.ARRAY_BUFFER, g_vbo); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, g_texCoordsOffset); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(2, 3, gl.FLOAT, false, 0, g_tangentsOffset); gl.enableVertexAttribArray(2); gl.vertexAttribPointer(3, 3, gl.FLOAT, false, 0, g_binormalsOffset); gl.enableVertexAttribArray(3); gl.vertexAttribPointer(4, 3, gl.FLOAT, false, 0, g_normalsOffset); gl.enableVertexAttribArray(4); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, g_elementVbo); checkGLError(); gl.drawElements(gl.TRIANGLES, g_numElements, gl.UNSIGNED_SHORT, 0); } // Array of images curently loading var g_loadingImages = []; // Clears all the images currently loading. // This is used to handle context lost events. function clearLoadingImages() { for (var ii = 0; ii < g_loadingImages.length; ++ii) { g_loadingImages[ii].onload = undefined; } g_loadingImages = []; } function loadTexture(src) { var texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); ++g_pendingTextureLoads; var image = new Image(); g_loadingImages.push(image); image.onload = function() { g_loadingImages.splice(g_loadingImages.indexOf(image), 1); --g_pendingTextureLoads; gl.bindTexture(gl.TEXTURE_2D, texture); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); checkGLError(); draw(); }; image.src = src; return texture; } function loadCubeMap(base, suffix) { var texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture); checkGLError(); gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); checkGLError(); gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); checkGLError(); // FIXME: TEXTURE_WRAP_R doesn't exist in OpenGL ES?! // gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE); // checkGLError(); gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR); checkGLError(); gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR); checkGLError(); var faces = [["posx", gl.TEXTURE_CUBE_MAP_POSITIVE_X], ["negx", gl.TEXTURE_CUBE_MAP_NEGATIVE_X], ["posy", gl.TEXTURE_CUBE_MAP_POSITIVE_Y], ["negy", gl.TEXTURE_CUBE_MAP_NEGATIVE_Y], ["posz", gl.TEXTURE_CUBE_MAP_POSITIVE_Z], ["negz", gl.TEXTURE_CUBE_MAP_NEGATIVE_Z]]; for (var i = 0; i < faces.length; i++) { var url = base + "-" + faces[i][0] + "." + suffix; var face = faces[i][1]; ++g_pendingTextureLoads; var image = new Image(); g_loadingImages.push(image); // Javascript has function, not block, scope. // See "JavaScript: The Good Parts", Chapter 4, "Functions", // section "Scope". image.onload = function(texture, face, image, url) { return function() { g_loadingImages.splice(g_loadingImages.indexOf(image), 1); --g_pendingTextureLoads; gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); gl.texImage2D( face, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); checkGLError(); draw(); } }(texture, face, image, url); console.log(url); image.src = url; } return texture; } ================================================ FILE: src/js/third_party/webgl_teapot/matrix4x4.js ================================================ /* * Copyright (c) 2009, Mozilla Corp * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * Based on sample code from the OpenGL(R) ES 2.0 Programming Guide, which carriers * the following header: * * Book: OpenGL(R) ES 2.0 Programming Guide * Authors: Aaftab Munshi, Dan Ginsburg, Dave Shreiner * ISBN-10: 0321502795 * ISBN-13: 9780321502797 * Publisher: Addison-Wesley Professional * URLs: http://safari.informit.com/9780321563835 * http://www.opengles-book.com */ // // A simple 4x4 Matrix utility class // function Matrix4x4() { this.elements = Array(16); this.loadIdentity(); } Matrix4x4.prototype = { scale: function (sx, sy, sz) { this.elements[0*4+0] *= sx; this.elements[0*4+1] *= sx; this.elements[0*4+2] *= sx; this.elements[0*4+3] *= sx; this.elements[1*4+0] *= sy; this.elements[1*4+1] *= sy; this.elements[1*4+2] *= sy; this.elements[1*4+3] *= sy; this.elements[2*4+0] *= sz; this.elements[2*4+1] *= sz; this.elements[2*4+2] *= sz; this.elements[2*4+3] *= sz; return this; }, translate: function (tx, ty, tz) { this.elements[3*4+0] += this.elements[0*4+0] * tx + this.elements[1*4+0] * ty + this.elements[2*4+0] * tz; this.elements[3*4+1] += this.elements[0*4+1] * tx + this.elements[1*4+1] * ty + this.elements[2*4+1] * tz; this.elements[3*4+2] += this.elements[0*4+2] * tx + this.elements[1*4+2] * ty + this.elements[2*4+2] * tz; this.elements[3*4+3] += this.elements[0*4+3] * tx + this.elements[1*4+3] * ty + this.elements[2*4+3] * tz; return this; }, rotate: function (angle, x, y, z) { var mag = Math.sqrt(x*x + y*y + z*z); var sinAngle = Math.sin(angle * Math.PI / 180.0); var cosAngle = Math.cos(angle * Math.PI / 180.0); if (mag > 0) { var xx, yy, zz, xy, yz, zx, xs, ys, zs; var oneMinusCos; var rotMat; x /= mag; y /= mag; z /= mag; xx = x * x; yy = y * y; zz = z * z; xy = x * y; yz = y * z; zx = z * x; xs = x * sinAngle; ys = y * sinAngle; zs = z * sinAngle; oneMinusCos = 1.0 - cosAngle; rotMat = new Matrix4x4(); rotMat.elements[0*4+0] = (oneMinusCos * xx) + cosAngle; rotMat.elements[0*4+1] = (oneMinusCos * xy) - zs; rotMat.elements[0*4+2] = (oneMinusCos * zx) + ys; rotMat.elements[0*4+3] = 0.0; rotMat.elements[1*4+0] = (oneMinusCos * xy) + zs; rotMat.elements[1*4+1] = (oneMinusCos * yy) + cosAngle; rotMat.elements[1*4+2] = (oneMinusCos * yz) - xs; rotMat.elements[1*4+3] = 0.0; rotMat.elements[2*4+0] = (oneMinusCos * zx) - ys; rotMat.elements[2*4+1] = (oneMinusCos * yz) + xs; rotMat.elements[2*4+2] = (oneMinusCos * zz) + cosAngle; rotMat.elements[2*4+3] = 0.0; rotMat.elements[3*4+0] = 0.0; rotMat.elements[3*4+1] = 0.0; rotMat.elements[3*4+2] = 0.0; rotMat.elements[3*4+3] = 1.0; rotMat = rotMat.multiply(this); this.elements = rotMat.elements; } return this; }, frustum: function (left, right, bottom, top, nearZ, farZ) { var deltaX = right - left; var deltaY = top - bottom; var deltaZ = farZ - nearZ; var frust; if ( (nearZ <= 0.0) || (farZ <= 0.0) || (deltaX <= 0.0) || (deltaY <= 0.0) || (deltaZ <= 0.0) ) return this; frust = new Matrix4x4(); frust.elements[0*4+0] = 2.0 * nearZ / deltaX; frust.elements[0*4+1] = frust.elements[0*4+2] = frust.elements[0*4+3] = 0.0; frust.elements[1*4+1] = 2.0 * nearZ / deltaY; frust.elements[1*4+0] = frust.elements[1*4+2] = frust.elements[1*4+3] = 0.0; frust.elements[2*4+0] = (right + left) / deltaX; frust.elements[2*4+1] = (top + bottom) / deltaY; frust.elements[2*4+2] = -(nearZ + farZ) / deltaZ; frust.elements[2*4+3] = -1.0; frust.elements[3*4+2] = -2.0 * nearZ * farZ / deltaZ; frust.elements[3*4+0] = frust.elements[3*4+1] = frust.elements[3*4+3] = 0.0; frust = frust.multiply(this); this.elements = frust.elements; return this; }, perspective: function (fovy, aspect, nearZ, farZ) { var frustumH = Math.tan(fovy / 360.0 * Math.PI) * nearZ; var frustumW = frustumH * aspect; return this.frustum(-frustumW, frustumW, -frustumH, frustumH, nearZ, farZ); }, ortho: function (left, right, bottom, top, nearZ, farZ) { var deltaX = right - left; var deltaY = top - bottom; var deltaZ = farZ - nearZ; var ortho = new Matrix4x4(); if ( (deltaX == 0.0) || (deltaY == 0.0) || (deltaZ == 0.0) ) return this; ortho.elements[0*4+0] = 2.0 / deltaX; ortho.elements[3*4+0] = -(right + left) / deltaX; ortho.elements[1*4+1] = 2.0 / deltaY; ortho.elements[3*4+1] = -(top + bottom) / deltaY; ortho.elements[2*4+2] = -2.0 / deltaZ; ortho.elements[3*4+2] = -(nearZ + farZ) / deltaZ; ortho = ortho.multiply(this); this.elements = ortho.elements; return this; }, multiply: function (right) { var tmp = new Matrix4x4(); for (var i = 0; i < 4; i++) { tmp.elements[i*4+0] = (this.elements[i*4+0] * right.elements[0*4+0]) + (this.elements[i*4+1] * right.elements[1*4+0]) + (this.elements[i*4+2] * right.elements[2*4+0]) + (this.elements[i*4+3] * right.elements[3*4+0]) ; tmp.elements[i*4+1] = (this.elements[i*4+0] * right.elements[0*4+1]) + (this.elements[i*4+1] * right.elements[1*4+1]) + (this.elements[i*4+2] * right.elements[2*4+1]) + (this.elements[i*4+3] * right.elements[3*4+1]) ; tmp.elements[i*4+2] = (this.elements[i*4+0] * right.elements[0*4+2]) + (this.elements[i*4+1] * right.elements[1*4+2]) + (this.elements[i*4+2] * right.elements[2*4+2]) + (this.elements[i*4+3] * right.elements[3*4+2]) ; tmp.elements[i*4+3] = (this.elements[i*4+0] * right.elements[0*4+3]) + (this.elements[i*4+1] * right.elements[1*4+3]) + (this.elements[i*4+2] * right.elements[2*4+3]) + (this.elements[i*4+3] * right.elements[3*4+3]) ; } this.elements = tmp.elements; return this; }, copy: function () { var tmp = new Matrix4x4(); for (var i = 0; i < 16; i++) { tmp.elements[i] = this.elements[i]; } return tmp; }, get: function (row, col) { return this.elements[4*row+col]; }, // In-place inversion invert: function () { var tmp_0 = this.get(2,2) * this.get(3,3); var tmp_1 = this.get(3,2) * this.get(2,3); var tmp_2 = this.get(1,2) * this.get(3,3); var tmp_3 = this.get(3,2) * this.get(1,3); var tmp_4 = this.get(1,2) * this.get(2,3); var tmp_5 = this.get(2,2) * this.get(1,3); var tmp_6 = this.get(0,2) * this.get(3,3); var tmp_7 = this.get(3,2) * this.get(0,3); var tmp_8 = this.get(0,2) * this.get(2,3); var tmp_9 = this.get(2,2) * this.get(0,3); var tmp_10 = this.get(0,2) * this.get(1,3); var tmp_11 = this.get(1,2) * this.get(0,3); var tmp_12 = this.get(2,0) * this.get(3,1); var tmp_13 = this.get(3,0) * this.get(2,1); var tmp_14 = this.get(1,0) * this.get(3,1); var tmp_15 = this.get(3,0) * this.get(1,1); var tmp_16 = this.get(1,0) * this.get(2,1); var tmp_17 = this.get(2,0) * this.get(1,1); var tmp_18 = this.get(0,0) * this.get(3,1); var tmp_19 = this.get(3,0) * this.get(0,1); var tmp_20 = this.get(0,0) * this.get(2,1); var tmp_21 = this.get(2,0) * this.get(0,1); var tmp_22 = this.get(0,0) * this.get(1,1); var tmp_23 = this.get(1,0) * this.get(0,1); var t0 = ((tmp_0 * this.get(1,1) + tmp_3 * this.get(2,1) + tmp_4 * this.get(3,1)) - (tmp_1 * this.get(1,1) + tmp_2 * this.get(2,1) + tmp_5 * this.get(3,1))); var t1 = ((tmp_1 * this.get(0,1) + tmp_6 * this.get(2,1) + tmp_9 * this.get(3,1)) - (tmp_0 * this.get(0,1) + tmp_7 * this.get(2,1) + tmp_8 * this.get(3,1))); var t2 = ((tmp_2 * this.get(0,1) + tmp_7 * this.get(1,1) + tmp_10 * this.get(3,1)) - (tmp_3 * this.get(0,1) + tmp_6 * this.get(1,1) + tmp_11 * this.get(3,1))); var t3 = ((tmp_5 * this.get(0,1) + tmp_8 * this.get(1,1) + tmp_11 * this.get(2,1)) - (tmp_4 * this.get(0,1) + tmp_9 * this.get(1,1) + tmp_10 * this.get(2,1))); var d = 1.0 / (this.get(0,0) * t0 + this.get(1,0) * t1 + this.get(2,0) * t2 + this.get(3,0) * t3); var out_00 = d * t0; var out_01 = d * t1; var out_02 = d * t2; var out_03 = d * t3; var out_10 = d * ((tmp_1 * this.get(1,0) + tmp_2 * this.get(2,0) + tmp_5 * this.get(3,0)) - (tmp_0 * this.get(1,0) + tmp_3 * this.get(2,0) + tmp_4 * this.get(3,0))); var out_11 = d * ((tmp_0 * this.get(0,0) + tmp_7 * this.get(2,0) + tmp_8 * this.get(3,0)) - (tmp_1 * this.get(0,0) + tmp_6 * this.get(2,0) + tmp_9 * this.get(3,0))); var out_12 = d * ((tmp_3 * this.get(0,0) + tmp_6 * this.get(1,0) + tmp_11 * this.get(3,0)) - (tmp_2 * this.get(0,0) + tmp_7 * this.get(1,0) + tmp_10 * this.get(3,0))); var out_13 = d * ((tmp_4 * this.get(0,0) + tmp_9 * this.get(1,0) + tmp_10 * this.get(2,0)) - (tmp_5 * this.get(0,0) + tmp_8 * this.get(1,0) + tmp_11 * this.get(2,0))); var out_20 = d * ((tmp_12 * this.get(1,3) + tmp_15 * this.get(2,3) + tmp_16 * this.get(3,3)) - (tmp_13 * this.get(1,3) + tmp_14 * this.get(2,3) + tmp_17 * this.get(3,3))); var out_21 = d * ((tmp_13 * this.get(0,3) + tmp_18 * this.get(2,3) + tmp_21 * this.get(3,3)) - (tmp_12 * this.get(0,3) + tmp_19 * this.get(2,3) + tmp_20 * this.get(3,3))); var out_22 = d * ((tmp_14 * this.get(0,3) + tmp_19 * this.get(1,3) + tmp_22 * this.get(3,3)) - (tmp_15 * this.get(0,3) + tmp_18 * this.get(1,3) + tmp_23 * this.get(3,3))); var out_23 = d * ((tmp_17 * this.get(0,3) + tmp_20 * this.get(1,3) + tmp_23 * this.get(2,3)) - (tmp_16 * this.get(0,3) + tmp_21 * this.get(1,3) + tmp_22 * this.get(2,3))); var out_30 = d * ((tmp_14 * this.get(2,2) + tmp_17 * this.get(3,2) + tmp_13 * this.get(1,2)) - (tmp_16 * this.get(3,2) + tmp_12 * this.get(1,2) + tmp_15 * this.get(2,2))); var out_31 = d * ((tmp_20 * this.get(3,2) + tmp_12 * this.get(0,2) + tmp_19 * this.get(2,2)) - (tmp_18 * this.get(2,2) + tmp_21 * this.get(3,2) + tmp_13 * this.get(0,2))); var out_32 = d * ((tmp_18 * this.get(1,2) + tmp_23 * this.get(3,2) + tmp_15 * this.get(0,2)) - (tmp_22 * this.get(3,2) + tmp_14 * this.get(0,2) + tmp_19 * this.get(1,2))); var out_33 = d * ((tmp_22 * this.get(2,2) + tmp_16 * this.get(0,2) + tmp_21 * this.get(1,2)) - (tmp_20 * this.get(1,2) + tmp_23 * this.get(2,2) + tmp_17 * this.get(0,2))); this.elements[0*4+0] = out_00; this.elements[0*4+1] = out_01; this.elements[0*4+2] = out_02; this.elements[0*4+3] = out_03; this.elements[1*4+0] = out_10; this.elements[1*4+1] = out_11; this.elements[1*4+2] = out_12; this.elements[1*4+3] = out_13; this.elements[2*4+0] = out_20; this.elements[2*4+1] = out_21; this.elements[2*4+2] = out_22; this.elements[2*4+3] = out_23; this.elements[3*4+0] = out_30; this.elements[3*4+1] = out_31; this.elements[3*4+2] = out_32; this.elements[3*4+3] = out_33; return this; }, // Returns new matrix which is the inverse of this inverse: function () { var tmp = this.copy(); return tmp.invert(); }, // In-place transpose transpose: function () { var tmp = this.elements[0*4+1]; this.elements[0*4+1] = this.elements[1*4+0]; this.elements[1*4+0] = tmp; tmp = this.elements[0*4+2]; this.elements[0*4+2] = this.elements[2*4+0]; this.elements[2*4+0] = tmp; tmp = this.elements[0*4+3]; this.elements[0*4+3] = this.elements[3*4+0]; this.elements[3*4+0] = tmp; tmp = this.elements[1*4+2]; this.elements[1*4+2] = this.elements[2*4+1]; this.elements[2*4+1] = tmp; tmp = this.elements[1*4+3]; this.elements[1*4+3] = this.elements[3*4+1]; this.elements[3*4+1] = tmp; tmp = this.elements[2*4+3]; this.elements[2*4+3] = this.elements[3*4+2]; this.elements[3*4+2] = tmp; return this; }, loadIdentity: function () { for (var i = 0; i < 16; i++) this.elements[i] = 0; this.elements[0*4+0] = 1.0; this.elements[1*4+1] = 1.0; this.elements[2*4+2] = 1.0; this.elements[3*4+3] = 1.0; return this; } }; ================================================ FILE: src/js/third_party/webgl_teapot/teapot-streams.js ================================================ /* * Copyright (c) 2009 The Chromium Authors. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var teapotPositions = new Float32Array([ 17.83489990234375, 0, 30.573999404907227, 16.452699661254883, -7.000179767608643, 30.573999404907227, 16.223100662231445, -6.902520179748535, 31.51460075378418, 17.586000442504883, 0, 31.51460075378418, 16.48940086364746, -7.015810012817383, 31.828100204467773, 17.87470054626465, 0, 31.828100204467773, 17.031099319458008, -7.246280193328857, 31.51460075378418, 18.46190071105957, 0, 31.51460075378418, 17.62779998779297, -7.500199794769287, 30.573999404907227, 19.108800888061523, 0, 30.573999404907227, 12.662699699401855, -12.662699699401855, 30.573999404907227, 12.486100196838379, -12.486100196838379, 31.51460075378418, 12.690999984741211, -12.690999984741211, 31.828100204467773, 13.10789966583252, -13.10789966583252, 31.51460075378418, 13.56719970703125, -13.56719970703125, 30.573999404907227, 7.000179767608643, -16.452699661254883, 30.573999404907227, 6.902520179748535, -16.223100662231445, 31.51460075378418, 7.015810012817383, -16.48940086364746, 31.828100204467773, 7.246280193328857, -17.031099319458008, 31.51460075378418, 7.500199794769287, -17.62779998779297, 30.573999404907227, 0, -17.83489990234375, 30.573999404907227, 0, -17.586000442504883, 31.51460075378418, 0, -17.87470054626465, 31.828100204467773, 0, -18.46190071105957, 31.51460075378418, 0, -19.108800888061523, 30.573999404907227, 0, -17.83489990234375, 30.573999404907227, -7.483870029449463, -16.452699661254883, 30.573999404907227, -7.106579780578613, -16.223100662231445, 31.51460075378418, 0, -17.586000442504883, 31.51460075378418, -7.07627010345459, -16.48940086364746, 31.828100204467773, 0, -17.87470054626465, 31.828100204467773, -7.25383996963501, -17.031099319458008, 31.51460075378418, 0, -18.46190071105957, 31.51460075378418, -7.500199794769287, -17.62779998779297, 30.573999404907227, 0, -19.108800888061523, 30.573999404907227, -13.092700004577637, -12.662699699401855, 30.573999404907227, -12.667499542236328, -12.486100196838379, 31.51460075378418, -12.744799613952637, -12.690999984741211, 31.828100204467773, -13.11460018157959, -13.10789966583252, 31.51460075378418, -13.56719970703125, -13.56719970703125, 30.573999404907227, -16.61389923095703, -7.000179767608643, 30.573999404907227, -16.291099548339844, -6.902520179748535, 31.51460075378418, -16.50950050354004, -7.015810012817383, 31.828100204467773, -17.033599853515625, -7.246280193328857, 31.51460075378418, -17.62779998779297, -7.500199794769287, 30.573999404907227, -17.83489990234375, 0, 30.573999404907227, -17.586000442504883, 0, 31.51460075378418, -17.87470054626465, 0, 31.828100204467773, -18.46190071105957, 0, 31.51460075378418, -19.108800888061523, 0, 30.573999404907227, -17.83489990234375, 0, 30.573999404907227, -16.452699661254883, 7.000179767608643, 30.573999404907227, -16.223100662231445, 6.902520179748535, 31.51460075378418, -17.586000442504883, 0, 31.51460075378418, -16.48940086364746, 7.015810012817383, 31.828100204467773, -17.87470054626465, 0, 31.828100204467773, -17.031099319458008, 7.246280193328857, 31.51460075378418, -18.46190071105957, 0, 31.51460075378418, -17.62779998779297, 7.500199794769287, 30.573999404907227, -19.108800888061523, 0, 30.573999404907227, -12.662699699401855, 12.662699699401855, 30.573999404907227, -12.486100196838379, 12.486100196838379, 31.51460075378418, -12.690999984741211, 12.690999984741211, 31.828100204467773, -13.10789966583252, 13.10789966583252, 31.51460075378418, -13.56719970703125, 13.56719970703125, 30.573999404907227, -7.000179767608643, 16.452699661254883, 30.573999404907227, -6.902520179748535, 16.223100662231445, 31.51460075378418, -7.015810012817383, 16.48940086364746, 31.828100204467773, -7.246280193328857, 17.031099319458008, 31.51460075378418, -7.500199794769287, 17.62779998779297, 30.573999404907227, 0, 17.83489990234375, 30.573999404907227, 0, 17.586000442504883, 31.51460075378418, 0, 17.87470054626465, 31.828100204467773, 0, 18.46190071105957, 31.51460075378418, 0, 19.108800888061523, 30.573999404907227, 0, 17.83489990234375, 30.573999404907227, 7.000179767608643, 16.452699661254883, 30.573999404907227, 6.902520179748535, 16.223100662231445, 31.51460075378418, 0, 17.586000442504883, 31.51460075378418, 7.015810012817383, 16.48940086364746, 31.828100204467773, 0, 17.87470054626465, 31.828100204467773, 7.246280193328857, 17.031099319458008, 31.51460075378418, 0, 18.46190071105957, 31.51460075378418, 7.500199794769287, 17.62779998779297, 30.573999404907227, 0, 19.108800888061523, 30.573999404907227, 12.662699699401855, 12.662699699401855, 30.573999404907227, 12.486100196838379, 12.486100196838379, 31.51460075378418, 12.690999984741211, 12.690999984741211, 31.828100204467773, 13.10789966583252, 13.10789966583252, 31.51460075378418, 13.56719970703125, 13.56719970703125, 30.573999404907227, 16.452699661254883, 7.000179767608643, 30.573999404907227, 16.223100662231445, 6.902520179748535, 31.51460075378418, 16.48940086364746, 7.015810012817383, 31.828100204467773, 17.031099319458008, 7.246280193328857, 31.51460075378418, 17.62779998779297, 7.500199794769287, 30.573999404907227, 17.83489990234375, 0, 30.573999404907227, 17.586000442504883, 0, 31.51460075378418, 17.87470054626465, 0, 31.828100204467773, 18.46190071105957, 0, 31.51460075378418, 19.108800888061523, 0, 30.573999404907227, 19.108800888061523, 0, 30.573999404907227, 17.62779998779297, -7.500199794769287, 30.573999404907227, 19.785400390625, -8.418190002441406, 25.572900772094727, 21.447599411010742, 0, 25.572900772094727, 21.667600631713867, -9.218990325927734, 20.661399841308594, 23.487899780273438, 0, 20.661399841308594, 22.99880027770996, -9.785409927368164, 15.928999900817871, 24.930999755859375, 0, 15.928999900817871, 23.503799438476562, -10.000300407409668, 11.465299606323242, 25.4783992767334, 0, 11.465299606323242, 13.56719970703125, -13.56719970703125, 30.573999404907227, 15.227800369262695, -15.227800369262695, 25.572900772094727, 16.67639923095703, -16.67639923095703, 20.661399841308594, 17.701000213623047, -17.701000213623047, 15.928999900817871, 18.089599609375, -18.089599609375, 11.465299606323242, 7.500199794769287, -17.62779998779297, 30.573999404907227, 8.418190002441406, -19.785400390625, 25.572900772094727, 9.218990325927734, -21.667600631713867, 20.661399841308594, 9.785409927368164, -22.99880027770996, 15.928999900817871, 10.000300407409668, -23.503799438476562, 11.465299606323242, 0, -19.108800888061523, 30.573999404907227, 0, -21.447599411010742, 25.572900772094727, 0, -23.487899780273438, 20.661399841308594, 0, -24.930999755859375, 15.928999900817871, 0, -25.4783992767334, 11.465299606323242, 0, -19.108800888061523, 30.573999404907227, -7.500199794769287, -17.62779998779297, 30.573999404907227, -8.418190002441406, -19.785400390625, 25.572900772094727, 0, -21.447599411010742, 25.572900772094727, -9.218990325927734, -21.667600631713867, 20.661399841308594, 0, -23.487899780273438, 20.661399841308594, -9.785409927368164, -22.99880027770996, 15.928999900817871, 0, -24.930999755859375, 15.928999900817871, -10.000300407409668, -23.503799438476562, 11.465299606323242, 0, -25.4783992767334, 11.465299606323242, -13.56719970703125, -13.56719970703125, 30.573999404907227, -15.227800369262695, -15.227800369262695, 25.572900772094727, -16.67639923095703, -16.67639923095703, 20.661399841308594, -17.701000213623047, -17.701000213623047, 15.928999900817871, -18.089599609375, -18.089599609375, 11.465299606323242, -17.62779998779297, -7.500199794769287, 30.573999404907227, -19.785400390625, -8.418190002441406, 25.572900772094727, -21.667600631713867, -9.218990325927734, 20.661399841308594, -22.99880027770996, -9.785409927368164, 15.928999900817871, -23.503799438476562, -10.000300407409668, 11.465299606323242, -19.108800888061523, 0, 30.573999404907227, -21.447599411010742, 0, 25.572900772094727, -23.487899780273438, 0, 20.661399841308594, -24.930999755859375, 0, 15.928999900817871, -25.4783992767334, 0, 11.465299606323242, -19.108800888061523, 0, 30.573999404907227, -17.62779998779297, 7.500199794769287, 30.573999404907227, -19.785400390625, 8.418190002441406, 25.572900772094727, -21.447599411010742, 0, 25.572900772094727, -21.667600631713867, 9.218990325927734, 20.661399841308594, -23.487899780273438, 0, 20.661399841308594, -22.99880027770996, 9.785409927368164, 15.928999900817871, -24.930999755859375, 0, 15.928999900817871, -23.503799438476562, 10.000300407409668, 11.465299606323242, -25.4783992767334, 0, 11.465299606323242, -13.56719970703125, 13.56719970703125, 30.573999404907227, -15.227800369262695, 15.227800369262695, 25.572900772094727, -16.67639923095703, 16.67639923095703, 20.661399841308594, -17.701000213623047, 17.701000213623047, 15.928999900817871, -18.089599609375, 18.089599609375, 11.465299606323242, -7.500199794769287, 17.62779998779297, 30.573999404907227, -8.418190002441406, 19.785400390625, 25.572900772094727, -9.218990325927734, 21.667600631713867, 20.661399841308594, -9.785409927368164, 22.99880027770996, 15.928999900817871, -10.000300407409668, 23.503799438476562, 11.465299606323242, 0, 19.108800888061523, 30.573999404907227, 0, 21.447599411010742, 25.572900772094727, 0, 23.487899780273438, 20.661399841308594, 0, 24.930999755859375, 15.928999900817871, 0, 25.4783992767334, 11.465299606323242, 0, 19.108800888061523, 30.573999404907227, 7.500199794769287, 17.62779998779297, 30.573999404907227, 8.418190002441406, 19.785400390625, 25.572900772094727, 0, 21.447599411010742, 25.572900772094727, 9.218990325927734, 21.667600631713867, 20.661399841308594, 0, 23.487899780273438, 20.661399841308594, 9.785409927368164, 22.99880027770996, 15.928999900817871, 0, 24.930999755859375, 15.928999900817871, 10.000300407409668, 23.503799438476562, 11.465299606323242, 0, 25.4783992767334, 11.465299606323242, 13.56719970703125, 13.56719970703125, 30.573999404907227, 15.227800369262695, 15.227800369262695, 25.572900772094727, 16.67639923095703, 16.67639923095703, 20.661399841308594, 17.701000213623047, 17.701000213623047, 15.928999900817871, 18.089599609375, 18.089599609375, 11.465299606323242, 17.62779998779297, 7.500199794769287, 30.573999404907227, 19.785400390625, 8.418190002441406, 25.572900772094727, 21.667600631713867, 9.218990325927734, 20.661399841308594, 22.99880027770996, 9.785409927368164, 15.928999900817871, 23.503799438476562, 10.000300407409668, 11.465299606323242, 19.108800888061523, 0, 30.573999404907227, 21.447599411010742, 0, 25.572900772094727, 23.487899780273438, 0, 20.661399841308594, 24.930999755859375, 0, 15.928999900817871, 25.4783992767334, 0, 11.465299606323242, 25.4783992767334, 0, 11.465299606323242, 23.503799438476562, -10.000300407409668, 11.465299606323242, 22.5856990814209, -9.609620094299316, 7.688300132751465, 24.48310089111328, 0, 7.688300132751465, 20.565799713134766, -8.750229835510254, 4.89661979675293, 22.29360008239746, 0, 4.89661979675293, 18.54599952697754, -7.890830039978027, 3.0006699562072754, 20.104000091552734, 0, 3.0006699562072754, 17.62779998779297, -7.500199794769287, 1.9108799695968628, 19.108800888061523, 0, 1.9108799695968628, 18.089599609375, -18.089599609375, 11.465299606323242, 17.382999420166016, -17.382999420166016, 7.688300132751465, 15.828399658203125, -15.828399658203125, 4.89661979675293, 14.273900032043457, -14.273900032043457, 3.0006699562072754, 13.56719970703125, -13.56719970703125, 1.9108799695968628, 10.000300407409668, -23.503799438476562, 11.465299606323242, 9.609620094299316, -22.5856990814209, 7.688300132751465, 8.750229835510254, -20.565799713134766, 4.89661979675293, 7.890830039978027, -18.54599952697754, 3.0006699562072754, 7.500199794769287, -17.62779998779297, 1.9108799695968628, 0, -25.4783992767334, 11.465299606323242, 0, -24.48310089111328, 7.688300132751465, 0, -22.29360008239746, 4.89661979675293, 0, -20.104000091552734, 3.0006699562072754, 0, -19.108800888061523, 1.9108799695968628, 0, -25.4783992767334, 11.465299606323242, -10.000300407409668, -23.503799438476562, 11.465299606323242, -9.609620094299316, -22.5856990814209, 7.688300132751465, 0, -24.48310089111328, 7.688300132751465, -8.750229835510254, -20.565799713134766, 4.89661979675293, 0, -22.29360008239746, 4.89661979675293, -7.890830039978027, -18.54599952697754, 3.0006699562072754, 0, -20.104000091552734, 3.0006699562072754, -7.500199794769287, -17.62779998779297, 1.9108799695968628, 0, -19.108800888061523, 1.9108799695968628, -18.089599609375, -18.089599609375, 11.465299606323242, -17.382999420166016, -17.382999420166016, 7.688300132751465, -15.828399658203125, -15.828399658203125, 4.89661979675293, -14.273900032043457, -14.273900032043457, 3.0006699562072754, -13.56719970703125, -13.56719970703125, 1.9108799695968628, -23.503799438476562, -10.000300407409668, 11.465299606323242, -22.5856990814209, -9.609620094299316, 7.688300132751465, -20.565799713134766, -8.750229835510254, 4.89661979675293, -18.54599952697754, -7.890830039978027, 3.0006699562072754, -17.62779998779297, -7.500199794769287, 1.9108799695968628, -25.4783992767334, 0, 11.465299606323242, -24.48310089111328, 0, 7.688300132751465, -22.29360008239746, 0, 4.89661979675293, -20.104000091552734, 0, 3.0006699562072754, -19.108800888061523, 0, 1.9108799695968628, -25.4783992767334, 0, 11.465299606323242, -23.503799438476562, 10.000300407409668, 11.465299606323242, -22.5856990814209, 9.609620094299316, 7.688300132751465, -24.48310089111328, 0, 7.688300132751465, -20.565799713134766, 8.750229835510254, 4.89661979675293, -22.29360008239746, 0, 4.89661979675293, -18.54599952697754, 7.890830039978027, 3.0006699562072754, -20.104000091552734, 0, 3.0006699562072754, -17.62779998779297, 7.500199794769287, 1.9108799695968628, -19.108800888061523, 0, 1.9108799695968628, -18.089599609375, 18.089599609375, 11.465299606323242, -17.382999420166016, 17.382999420166016, 7.688300132751465, -15.828399658203125, 15.828399658203125, 4.89661979675293, -14.273900032043457, 14.273900032043457, 3.0006699562072754, -13.56719970703125, 13.56719970703125, 1.9108799695968628, -10.000300407409668, 23.503799438476562, 11.465299606323242, -9.609620094299316, 22.5856990814209, 7.688300132751465, -8.750229835510254, 20.565799713134766, 4.89661979675293, -7.890830039978027, 18.54599952697754, 3.0006699562072754, -7.500199794769287, 17.62779998779297, 1.9108799695968628, 0, 25.4783992767334, 11.465299606323242, 0, 24.48310089111328, 7.688300132751465, 0, 22.29360008239746, 4.89661979675293, 0, 20.104000091552734, 3.0006699562072754, 0, 19.108800888061523, 1.9108799695968628, 0, 25.4783992767334, 11.465299606323242, 10.000300407409668, 23.503799438476562, 11.465299606323242, 9.609620094299316, 22.5856990814209, 7.688300132751465, 0, 24.48310089111328, 7.688300132751465, 8.750229835510254, 20.565799713134766, 4.89661979675293, 0, 22.29360008239746, 4.89661979675293, 7.890830039978027, 18.54599952697754, 3.0006699562072754, 0, 20.104000091552734, 3.0006699562072754, 7.500199794769287, 17.62779998779297, 1.9108799695968628, 0, 19.108800888061523, 1.9108799695968628, 18.089599609375, 18.089599609375, 11.465299606323242, 17.382999420166016, 17.382999420166016, 7.688300132751465, 15.828399658203125, 15.828399658203125, 4.89661979675293, 14.273900032043457, 14.273900032043457, 3.0006699562072754, 13.56719970703125, 13.56719970703125, 1.9108799695968628, 23.503799438476562, 10.000300407409668, 11.465299606323242, 22.5856990814209, 9.609620094299316, 7.688300132751465, 20.565799713134766, 8.750229835510254, 4.89661979675293, 18.54599952697754, 7.890830039978027, 3.0006699562072754, 17.62779998779297, 7.500199794769287, 1.9108799695968628, 25.4783992767334, 0, 11.465299606323242, 24.48310089111328, 0, 7.688300132751465, 22.29360008239746, 0, 4.89661979675293, 20.104000091552734, 0, 3.0006699562072754, 19.108800888061523, 0, 1.9108799695968628, 19.108800888061523, 0, 1.9108799695968628, 17.62779998779297, -7.500199794769287, 1.9108799695968628, 17.228500366210938, -7.330269813537598, 1.2092299461364746, 18.675800323486328, 0, 1.2092299461364746, 15.093799591064453, -6.422039985656738, 0.5971490144729614, 16.361900329589844, 0, 0.5971490144729614, 9.819259643554688, -4.177840232849121, 0.16421599686145782, 10.644200325012207, 0, 0.16421599686145782, 0, 0, 0, 0, 0, 0, 13.56719970703125, -13.56719970703125, 1.9108799695968628, 13.25979995727539, -13.25979995727539, 1.2092299461364746, 11.616900444030762, -11.616900444030762, 0.5971490144729614, 7.557370185852051, -7.557370185852051, 0.16421599686145782, 0, 0, 0, 7.500199794769287, -17.62779998779297, 1.9108799695968628, 7.330269813537598, -17.228500366210938, 1.2092299461364746, 6.422039985656738, -15.093799591064453, 0.5971490144729614, 4.177840232849121, -9.819259643554688, 0.16421599686145782, 0, 0, 0, 0, -19.108800888061523, 1.9108799695968628, 0, -18.675800323486328, 1.2092299461364746, 0, -16.361900329589844, 0.5971490144729614, 0, -10.644200325012207, 0.16421599686145782, 0, 0, 0, 0, -19.108800888061523, 1.9108799695968628, -7.500199794769287, -17.62779998779297, 1.9108799695968628, -7.330269813537598, -17.228500366210938, 1.2092299461364746, 0, -18.675800323486328, 1.2092299461364746, -6.422039985656738, -15.093799591064453, 0.5971490144729614, 0, -16.361900329589844, 0.5971490144729614, -4.177840232849121, -9.819259643554688, 0.16421599686145782, 0, -10.644200325012207, 0.16421599686145782, 0, 0, 0, 0, 0, 0, -13.56719970703125, -13.56719970703125, 1.9108799695968628, -13.25979995727539, -13.25979995727539, 1.2092299461364746, -11.616900444030762, -11.616900444030762, 0.5971490144729614, -7.557370185852051, -7.557370185852051, 0.16421599686145782, 0, 0, 0, -17.62779998779297, -7.500199794769287, 1.9108799695968628, -17.228500366210938, -7.330269813537598, 1.2092299461364746, -15.093799591064453, -6.422039985656738, 0.5971490144729614, -9.819259643554688, -4.177840232849121, 0.16421599686145782, 0, 0, 0, -19.108800888061523, 0, 1.9108799695968628, -18.675800323486328, 0, 1.2092299461364746, -16.361900329589844, 0, 0.5971490144729614, -10.644200325012207, 0, 0.16421599686145782, 0, 0, 0, -19.108800888061523, 0, 1.9108799695968628, -17.62779998779297, 7.500199794769287, 1.9108799695968628, -17.228500366210938, 7.330269813537598, 1.2092299461364746, -18.675800323486328, 0, 1.2092299461364746, -15.093799591064453, 6.422039985656738, 0.5971490144729614, -16.361900329589844, 0, 0.5971490144729614, -9.819259643554688, 4.177840232849121, 0.16421599686145782, -10.644200325012207, 0, 0.16421599686145782, 0, 0, 0, 0, 0, 0, -13.56719970703125, 13.56719970703125, 1.9108799695968628, -13.25979995727539, 13.25979995727539, 1.2092299461364746, -11.616900444030762, 11.616900444030762, 0.5971490144729614, -7.557370185852051, 7.557370185852051, 0.16421599686145782, 0, 0, 0, -7.500199794769287, 17.62779998779297, 1.9108799695968628, -7.330269813537598, 17.228500366210938, 1.2092299461364746, -6.422039985656738, 15.093799591064453, 0.5971490144729614, -4.177840232849121, 9.819259643554688, 0.16421599686145782, 0, 0, 0, 0, 19.108800888061523, 1.9108799695968628, 0, 18.675800323486328, 1.2092299461364746, 0, 16.361900329589844, 0.5971490144729614, 0, 10.644200325012207, 0.16421599686145782, 0, 0, 0, 0, 19.108800888061523, 1.9108799695968628, 7.500199794769287, 17.62779998779297, 1.9108799695968628, 7.330269813537598, 17.228500366210938, 1.2092299461364746, 0, 18.675800323486328, 1.2092299461364746, 6.422039985656738, 15.093799591064453, 0.5971490144729614, 0, 16.361900329589844, 0.5971490144729614, 4.177840232849121, 9.819259643554688, 0.16421599686145782, 0, 10.644200325012207, 0.16421599686145782, 0, 0, 0, 0, 0, 0, 13.56719970703125, 13.56719970703125, 1.9108799695968628, 13.25979995727539, 13.25979995727539, 1.2092299461364746, 11.616900444030762, 11.616900444030762, 0.5971490144729614, 7.557370185852051, 7.557370185852051, 0.16421599686145782, 0, 0, 0, 17.62779998779297, 7.500199794769287, 1.9108799695968628, 17.228500366210938, 7.330269813537598, 1.2092299461364746, 15.093799591064453, 6.422039985656738, 0.5971490144729614, 9.819259643554688, 4.177840232849121, 0.16421599686145782, 0, 0, 0, 19.108800888061523, 0, 1.9108799695968628, 18.675800323486328, 0, 1.2092299461364746, 16.361900329589844, 0, 0.5971490144729614, 10.644200325012207, 0, 0.16421599686145782, 0, 0, 0, -20.382699966430664, 0, 25.796899795532227, -20.1835994720459, -2.149739980697632, 26.244699478149414, -26.511600494384766, -2.149739980697632, 26.192899703979492, -26.334299087524414, 0, 25.752099990844727, -31.156299591064453, -2.149739980697632, 25.830400466918945, -30.733299255371094, 0, 25.438600540161133, -34.016998291015625, -2.149739980697632, 24.846500396728516, -33.46030044555664, 0, 24.587600708007812, -34.99290084838867, -2.149739980697632, 22.930500030517578, -34.39580154418945, 0, 22.930500030517578, -19.74570083618164, -2.8663198947906494, 27.229999542236328, -26.901599884033203, -2.8663198947906494, 27.162799835205078, -32.08679962158203, -2.8663198947906494, 26.69260025024414, -35.241798400878906, -2.8663198947906494, 25.416200637817383, -36.30670166015625, -2.8663198947906494, 22.930500030517578, -19.30780029296875, -2.149739980697632, 28.215299606323242, -27.29159927368164, -2.149739980697632, 28.132699966430664, -33.017398834228516, -2.149739980697632, 27.55470085144043, -36.46649932861328, -2.149739980697632, 25.98579978942871, -37.620399475097656, -2.149739980697632, 22.930500030517578, -19.108800888061523, 0, 28.66320037841797, -27.468900680541992, 0, 28.57360076904297, -33.440399169921875, 0, 27.94659996032715, -37.02330017089844, 0, 26.244699478149414, -38.21760177612305, 0, 22.930500030517578, -19.108800888061523, 0, 28.66320037841797, -19.30780029296875, 2.149739980697632, 28.215299606323242, -27.29159927368164, 2.149739980697632, 28.132699966430664, -27.468900680541992, 0, 28.57360076904297, -33.017398834228516, 2.149739980697632, 27.55470085144043, -33.440399169921875, 0, 27.94659996032715, -36.46649932861328, 2.149739980697632, 25.98579978942871, -37.02330017089844, 0, 26.244699478149414, -37.620399475097656, 2.149739980697632, 22.930500030517578, -38.21760177612305, 0, 22.930500030517578, -19.74570083618164, 2.8663198947906494, 27.229999542236328, -26.901599884033203, 2.8663198947906494, 27.162799835205078, -32.08679962158203, 2.8663198947906494, 26.69260025024414, -35.241798400878906, 2.8663198947906494, 25.416200637817383, -36.30670166015625, 2.8663198947906494, 22.930500030517578, -20.1835994720459, 2.149739980697632, 26.244699478149414, -26.511600494384766, 2.149739980697632, 26.192899703979492, -31.156299591064453, 2.149739980697632, 25.830400466918945, -34.016998291015625, 2.149739980697632, 24.846500396728516, -34.99290084838867, 2.149739980697632, 22.930500030517578, -20.382699966430664, 0, 25.796899795532227, -26.334299087524414, 0, 25.752099990844727, -30.733299255371094, 0, 25.438600540161133, -33.46030044555664, 0, 24.587600708007812, -34.39580154418945, 0, 22.930500030517578, -34.39580154418945, 0, 22.930500030517578, -34.99290084838867, -2.149739980697632, 22.930500030517578, -34.44089889526367, -2.149739980697632, 20.082199096679688, -33.89820098876953, 0, 20.33289909362793, -32.711299896240234, -2.149739980697632, 16.81529998779297, -32.32569885253906, 0, 17.197900772094727, -29.69420051574707, -2.149739980697632, 13.590499877929688, -29.558900833129883, 0, 14.062899589538574, -25.279300689697266, -2.149739980697632, 10.8681001663208, -25.4783992767334, 0, 11.465299606323242, -36.30670166015625, -2.8663198947906494, 22.930500030517578, -35.6348991394043, -2.8663198947906494, 19.530500411987305, -33.55979919433594, -2.8663198947906494, 15.973699569702148, -29.99180030822754, -2.8663198947906494, 12.551300048828125, -24.841400146484375, -2.8663198947906494, 9.554389953613281, -37.620399475097656, -2.149739980697632, 22.930500030517578, -36.82889938354492, -2.149739980697632, 18.97879981994629, -34.408199310302734, -2.149739980697632, 15.132100105285645, -30.289499282836914, -2.149739980697632, 11.512200355529785, -24.403499603271484, -2.149739980697632, 8.240659713745117, -38.21760177612305, 0, 22.930500030517578, -37.37160110473633, 0, 18.728099822998047, -34.79389953613281, 0, 14.749600410461426, -30.424800872802734, 0, 11.039799690246582, -24.204500198364258, 0, 7.643509864807129, -38.21760177612305, 0, 22.930500030517578, -37.620399475097656, 2.149739980697632, 22.930500030517578, -36.82889938354492, 2.149739980697632, 18.97879981994629, -37.37160110473633, 0, 18.728099822998047, -34.408199310302734, 2.149739980697632, 15.132100105285645, -34.79389953613281, 0, 14.749600410461426, -30.289499282836914, 2.149739980697632, 11.512200355529785, -30.424800872802734, 0, 11.039799690246582, -24.403499603271484, 2.149739980697632, 8.240659713745117, -24.204500198364258, 0, 7.643509864807129, -36.30670166015625, 2.8663198947906494, 22.930500030517578, -35.6348991394043, 2.8663198947906494, 19.530500411987305, -33.55979919433594, 2.8663198947906494, 15.973699569702148, -29.99180030822754, 2.8663198947906494, 12.551300048828125, -24.841400146484375, 2.8663198947906494, 9.554389953613281, -34.99290084838867, 2.149739980697632, 22.930500030517578, -34.44089889526367, 2.149739980697632, 20.082199096679688, -32.711299896240234, 2.149739980697632, 16.81529998779297, -29.69420051574707, 2.149739980697632, 13.590499877929688, -25.279300689697266, 2.149739980697632, 10.8681001663208, -34.39580154418945, 0, 22.930500030517578, -33.89820098876953, 0, 20.33289909362793, -32.32569885253906, 0, 17.197900772094727, -29.558900833129883, 0, 14.062899589538574, -25.4783992767334, 0, 11.465299606323242, 21.656600952148438, 0, 18.15329933166504, 21.656600952148438, -4.729420185089111, 16.511199951171875, 28.233999252319336, -4.270359992980957, 18.339000701904297, 27.76740074157715, 0, 19.55660057067871, 31.011899948120117, -3.2604401111602783, 22.221399307250977, 30.4148006439209, 0, 22.930500030517578, 32.59560012817383, -2.2505099773406982, 26.764400482177734, 31.867900848388672, 0, 27.020999908447266, 35.5900993347168, -1.791450023651123, 30.573999404907227, 34.39580154418945, 0, 30.573999404907227, 21.656600952148438, -6.3059000968933105, 12.89840030670166, 29.260299682617188, -5.693819999694824, 15.660200119018555, 32.32569885253906, -4.347249984741211, 20.661399841308594, 34.19670104980469, -3.0006699562072754, 26.199899673461914, 38.21760177612305, -2.3886001110076904, 30.573999404907227, 21.656600952148438, -4.729420185089111, 9.285670280456543, 30.286699295043945, -4.270359992980957, 12.981499671936035, 33.639400482177734, -3.2604401111602783, 19.101299285888672, 35.79790115356445, -2.2505099773406982, 25.635400772094727, 40.845001220703125, -1.791450023651123, 30.573999404907227, 21.656600952148438, 0, 7.643509864807129, 30.75320053100586, 0, 11.763799667358398, 34.23659896850586, 0, 18.392200469970703, 36.52560043334961, 0, 25.378799438476562, 42.03929901123047, 0, 30.573999404907227, 21.656600952148438, 0, 7.643509864807129, 21.656600952148438, 4.729420185089111, 9.285670280456543, 30.286699295043945, 4.270359992980957, 12.981499671936035, 30.75320053100586, 0, 11.763799667358398, 33.639400482177734, 3.2604401111602783, 19.101299285888672, 34.23659896850586, 0, 18.392200469970703, 35.79790115356445, 2.2505099773406982, 25.635400772094727, 36.52560043334961, 0, 25.378799438476562, 40.845001220703125, 1.791450023651123, 30.573999404907227, 42.03929901123047, 0, 30.573999404907227, 21.656600952148438, 6.3059000968933105, 12.89840030670166, 29.260299682617188, 5.693819999694824, 15.660200119018555, 32.32569885253906, 4.347249984741211, 20.661399841308594, 34.19670104980469, 3.0006699562072754, 26.199899673461914, 38.21760177612305, 2.3886001110076904, 30.573999404907227, 21.656600952148438, 4.729420185089111, 16.511199951171875, 28.233999252319336, 4.270359992980957, 18.339000701904297, 31.011899948120117, 3.2604401111602783, 22.221399307250977, 32.59560012817383, 2.2505099773406982, 26.764400482177734, 35.5900993347168, 1.791450023651123, 30.573999404907227, 21.656600952148438, 0, 18.15329933166504, 27.76740074157715, 0, 19.55660057067871, 30.4148006439209, 0, 22.930500030517578, 31.867900848388672, 0, 27.020999908447266, 34.39580154418945, 0, 30.573999404907227, 34.39580154418945, 0, 30.573999404907227, 35.5900993347168, -1.791450023651123, 30.573999404907227, 36.59049987792969, -1.679479956626892, 31.137699127197266, 35.3114013671875, 0, 31.111499786376953, 37.18870162963867, -1.4331599473953247, 31.332599639892578, 35.98820114135742, 0, 31.290599822998047, 37.206600189208984, -1.1868300437927246, 31.1481990814209, 36.187198638916016, 0, 31.111499786376953, 36.46590042114258, -1.074869990348816, 30.573999404907227, 35.669700622558594, 0, 30.573999404907227, 38.21760177612305, -2.3886001110076904, 30.573999404907227, 39.40439987182617, -2.2393100261688232, 31.195499420166016, 39.829898834228516, -1.9108799695968628, 31.424999237060547, 39.44919967651367, -1.582450032234192, 31.229000091552734, 38.21760177612305, -1.4331599473953247, 30.573999404907227, 40.845001220703125, -1.791450023651123, 30.573999404907227, 42.218299865722656, -1.679479956626892, 31.25320053100586, 42.47100067138672, -1.4331599473953247, 31.51740074157715, 41.69169998168945, -1.1868300437927246, 31.309900283813477, 39.969200134277344, -1.074869990348816, 30.573999404907227, 42.03929901123047, 0, 30.573999404907227, 43.49729919433594, 0, 31.279399871826172, 43.67150115966797, 0, 31.55929946899414, 42.71110153198242, 0, 31.346599578857422, 40.76539993286133, 0, 30.573999404907227, 42.03929901123047, 0, 30.573999404907227, 40.845001220703125, 1.791450023651123, 30.573999404907227, 42.218299865722656, 1.679479956626892, 31.25320053100586, 43.49729919433594, 0, 31.279399871826172, 42.47100067138672, 1.4331599473953247, 31.51740074157715, 43.67150115966797, 0, 31.55929946899414, 41.69169998168945, 1.1868300437927246, 31.309900283813477, 42.71110153198242, 0, 31.346599578857422, 39.969200134277344, 1.074869990348816, 30.573999404907227, 40.76539993286133, 0, 30.573999404907227, 38.21760177612305, 2.3886001110076904, 30.573999404907227, 39.40439987182617, 2.2393100261688232, 31.195499420166016, 39.829898834228516, 1.9108799695968628, 31.424999237060547, 39.44919967651367, 1.582450032234192, 31.229000091552734, 38.21760177612305, 1.4331599473953247, 30.573999404907227, 35.5900993347168, 1.791450023651123, 30.573999404907227, 36.59049987792969, 1.679479956626892, 31.137699127197266, 37.18870162963867, 1.4331599473953247, 31.332599639892578, 37.206600189208984, 1.1868300437927246, 31.1481990814209, 36.46590042114258, 1.074869990348816, 30.573999404907227, 34.39580154418945, 0, 30.573999404907227, 35.3114013671875, 0, 31.111499786376953, 35.98820114135742, 0, 31.290599822998047, 36.187198638916016, 0, 31.111499786376953, 35.669700622558594, 0, 30.573999404907227, 0, 0, 40.12839889526367, 0, 0, 40.12839889526367, 4.004499912261963, -1.7077000141143799, 39.501399993896484, 4.339280128479004, 0, 39.501399993896484, 3.8207099437713623, -1.6290700435638428, 37.97869873046875, 4.140230178833008, 0, 37.97869873046875, 2.314160108566284, -0.985912024974823, 36.09769821166992, 2.5080299377441406, 0, 36.09769821166992, 2.3503799438476562, -1.0000300407409668, 34.39580154418945, 2.547840118408203, 0, 34.39580154418945, 0, 0, 40.12839889526367, 3.0849199295043945, -3.0849199295043945, 39.501399993896484, 2.943150043487549, -2.943150043487549, 37.97869873046875, 1.782039999961853, -1.782039999961853, 36.09769821166992, 1.8089599609375, -1.8089599609375, 34.39580154418945, 0, 0, 40.12839889526367, 1.7077000141143799, -4.004499912261963, 39.501399993896484, 1.6290700435638428, -3.8207099437713623, 37.97869873046875, 0.985912024974823, -2.314160108566284, 36.09769821166992, 1.0000300407409668, -2.3503799438476562, 34.39580154418945, 0, 0, 40.12839889526367, 0, -4.339280128479004, 39.501399993896484, 0, -4.140230178833008, 37.97869873046875, 0, -2.5080299377441406, 36.09769821166992, 0, -2.547840118408203, 34.39580154418945, 0, 0, 40.12839889526367, 0, 0, 40.12839889526367, -1.7077000141143799, -4.004499912261963, 39.501399993896484, 0, -4.339280128479004, 39.501399993896484, -1.6290700435638428, -3.8207099437713623, 37.97869873046875, 0, -4.140230178833008, 37.97869873046875, -0.985912024974823, -2.314160108566284, 36.09769821166992, 0, -2.5080299377441406, 36.09769821166992, -1.0000300407409668, -2.3503799438476562, 34.39580154418945, 0, -2.547840118408203, 34.39580154418945, 0, 0, 40.12839889526367, -3.0849199295043945, -3.0849199295043945, 39.501399993896484, -2.943150043487549, -2.943150043487549, 37.97869873046875, -1.782039999961853, -1.782039999961853, 36.09769821166992, -1.8089599609375, -1.8089599609375, 34.39580154418945, 0, 0, 40.12839889526367, -4.004499912261963, -1.7077000141143799, 39.501399993896484, -3.8207099437713623, -1.6290700435638428, 37.97869873046875, -2.314160108566284, -0.985912024974823, 36.09769821166992, -2.3503799438476562, -1.0000300407409668, 34.39580154418945, 0, 0, 40.12839889526367, -4.339280128479004, 0, 39.501399993896484, -4.140230178833008, 0, 37.97869873046875, -2.5080299377441406, 0, 36.09769821166992, -2.547840118408203, 0, 34.39580154418945, 0, 0, 40.12839889526367, 0, 0, 40.12839889526367, -4.004499912261963, 1.7077000141143799, 39.501399993896484, -4.339280128479004, 0, 39.501399993896484, -3.8207099437713623, 1.6290700435638428, 37.97869873046875, -4.140230178833008, 0, 37.97869873046875, -2.314160108566284, 0.985912024974823, 36.09769821166992, -2.5080299377441406, 0, 36.09769821166992, -2.3503799438476562, 1.0000300407409668, 34.39580154418945, -2.547840118408203, 0, 34.39580154418945, 0, 0, 40.12839889526367, -3.0849199295043945, 3.0849199295043945, 39.501399993896484, -2.943150043487549, 2.943150043487549, 37.97869873046875, -1.782039999961853, 1.782039999961853, 36.09769821166992, -1.8089599609375, 1.8089599609375, 34.39580154418945, 0, 0, 40.12839889526367, -1.7077000141143799, 4.004499912261963, 39.501399993896484, -1.6290700435638428, 3.8207099437713623, 37.97869873046875, -0.985912024974823, 2.314160108566284, 36.09769821166992, -1.0000300407409668, 2.3503799438476562, 34.39580154418945, 0, 0, 40.12839889526367, 0, 4.339280128479004, 39.501399993896484, 0, 4.140230178833008, 37.97869873046875, 0, 2.5080299377441406, 36.09769821166992, 0, 2.547840118408203, 34.39580154418945, 0, 0, 40.12839889526367, 0, 0, 40.12839889526367, 1.7077000141143799, 4.004499912261963, 39.501399993896484, 0, 4.339280128479004, 39.501399993896484, 1.6290700435638428, 3.8207099437713623, 37.97869873046875, 0, 4.140230178833008, 37.97869873046875, 0.985912024974823, 2.314160108566284, 36.09769821166992, 0, 2.5080299377441406, 36.09769821166992, 1.0000300407409668, 2.3503799438476562, 34.39580154418945, 0, 2.547840118408203, 34.39580154418945, 0, 0, 40.12839889526367, 3.0849199295043945, 3.0849199295043945, 39.501399993896484, 2.943150043487549, 2.943150043487549, 37.97869873046875, 1.782039999961853, 1.782039999961853, 36.09769821166992, 1.8089599609375, 1.8089599609375, 34.39580154418945, 0, 0, 40.12839889526367, 4.004499912261963, 1.7077000141143799, 39.501399993896484, 3.8207099437713623, 1.6290700435638428, 37.97869873046875, 2.314160108566284, 0.985912024974823, 36.09769821166992, 2.3503799438476562, 1.0000300407409668, 34.39580154418945, 0, 0, 40.12839889526367, 4.339280128479004, 0, 39.501399993896484, 4.140230178833008, 0, 37.97869873046875, 2.5080299377441406, 0, 36.09769821166992, 2.547840118408203, 0, 34.39580154418945, 2.547840118408203, 0, 34.39580154418945, 2.3503799438476562, -1.0000300407409668, 34.39580154418945, 5.361800193786621, -2.2813100814819336, 33.261199951171875, 5.812250137329102, 0, 33.261199951171875, 9.695320129394531, -4.125110149383545, 32.484901428222656, 10.50979995727539, 0, 32.484901428222656, 13.58810043334961, -5.781400203704834, 31.708599090576172, 14.729700088500977, 0, 31.708599090576172, 15.27750015258789, -6.5001702308654785, 30.573999404907227, 16.56089973449707, 0, 30.573999404907227, 1.8089599609375, -1.8089599609375, 34.39580154418945, 4.126699924468994, -4.126699924468994, 33.261199951171875, 7.461979866027832, -7.461979866027832, 32.484901428222656, 10.458100318908691, -10.458100318908691, 31.708599090576172, 11.758299827575684, -11.758299827575684, 30.573999404907227, 1.0000300407409668, -2.3503799438476562, 34.39580154418945, 2.2813100814819336, -5.361800193786621, 33.261199951171875, 4.125110149383545, -9.695320129394531, 32.484901428222656, 5.781400203704834, -13.58810043334961, 31.708599090576172, 6.5001702308654785, -15.27750015258789, 30.573999404907227, 0, -2.547840118408203, 34.39580154418945, 0, -5.812250137329102, 33.261199951171875, 0, -10.50979995727539, 32.484901428222656, 0, -14.729700088500977, 31.708599090576172, 0, -16.56089973449707, 30.573999404907227, 0, -2.547840118408203, 34.39580154418945, -1.0000300407409668, -2.3503799438476562, 34.39580154418945, -2.2813100814819336, -5.361800193786621, 33.261199951171875, 0, -5.812250137329102, 33.261199951171875, -4.125110149383545, -9.695320129394531, 32.484901428222656, 0, -10.50979995727539, 32.484901428222656, -5.781400203704834, -13.58810043334961, 31.708599090576172, 0, -14.729700088500977, 31.708599090576172, -6.5001702308654785, -15.27750015258789, 30.573999404907227, 0, -16.56089973449707, 30.573999404907227, -1.8089599609375, -1.8089599609375, 34.39580154418945, -4.126699924468994, -4.126699924468994, 33.261199951171875, -7.461979866027832, -7.461979866027832, 32.484901428222656, -10.458100318908691, -10.458100318908691, 31.708599090576172, -11.758299827575684, -11.758299827575684, 30.573999404907227, -2.3503799438476562, -1.0000300407409668, 34.39580154418945, -5.361800193786621, -2.2813100814819336, 33.261199951171875, -9.695320129394531, -4.125110149383545, 32.484901428222656, -13.58810043334961, -5.781400203704834, 31.708599090576172, -15.27750015258789, -6.5001702308654785, 30.573999404907227, -2.547840118408203, 0, 34.39580154418945, -5.812250137329102, 0, 33.261199951171875, -10.50979995727539, 0, 32.484901428222656, -14.729700088500977, 0, 31.708599090576172, -16.56089973449707, 0, 30.573999404907227, -2.547840118408203, 0, 34.39580154418945, -2.3503799438476562, 1.0000300407409668, 34.39580154418945, -5.361800193786621, 2.2813100814819336, 33.261199951171875, -5.812250137329102, 0, 33.261199951171875, -9.695320129394531, 4.125110149383545, 32.484901428222656, -10.50979995727539, 0, 32.484901428222656, -13.58810043334961, 5.781400203704834, 31.708599090576172, -14.729700088500977, 0, 31.708599090576172, -15.27750015258789, 6.5001702308654785, 30.573999404907227, -16.56089973449707, 0, 30.573999404907227, -1.8089599609375, 1.8089599609375, 34.39580154418945, -4.126699924468994, 4.126699924468994, 33.261199951171875, -7.461979866027832, 7.461979866027832, 32.484901428222656, -10.458100318908691, 10.458100318908691, 31.708599090576172, -11.758299827575684, 11.758299827575684, 30.573999404907227, -1.0000300407409668, 2.3503799438476562, 34.39580154418945, -2.2813100814819336, 5.361800193786621, 33.261199951171875, -4.125110149383545, 9.695320129394531, 32.484901428222656, -5.781400203704834, 13.58810043334961, 31.708599090576172, -6.5001702308654785, 15.27750015258789, 30.573999404907227, 0, 2.547840118408203, 34.39580154418945, 0, 5.812250137329102, 33.261199951171875, 0, 10.50979995727539, 32.484901428222656, 0, 14.729700088500977, 31.708599090576172, 0, 16.56089973449707, 30.573999404907227, 0, 2.547840118408203, 34.39580154418945, 1.0000300407409668, 2.3503799438476562, 34.39580154418945, 2.2813100814819336, 5.361800193786621, 33.261199951171875, 0, 5.812250137329102, 33.261199951171875, 4.125110149383545, 9.695320129394531, 32.484901428222656, 0, 10.50979995727539, 32.484901428222656, 5.781400203704834, 13.58810043334961, 31.708599090576172, 0, 14.729700088500977, 31.708599090576172, 6.5001702308654785, 15.27750015258789, 30.573999404907227, 0, 16.56089973449707, 30.573999404907227, 1.8089599609375, 1.8089599609375, 34.39580154418945, 4.126699924468994, 4.126699924468994, 33.261199951171875, 7.461979866027832, 7.461979866027832, 32.484901428222656, 10.458100318908691, 10.458100318908691, 31.708599090576172, 11.758299827575684, 11.758299827575684, 30.573999404907227, 2.3503799438476562, 1.0000300407409668, 34.39580154418945, 5.361800193786621, 2.2813100814819336, 33.261199951171875, 9.695320129394531, 4.125110149383545, 32.484901428222656, 13.58810043334961, 5.781400203704834, 31.708599090576172, 15.27750015258789, 6.5001702308654785, 30.573999404907227, 2.547840118408203, 0, 34.39580154418945, 5.812250137329102, 0, 33.261199951171875, 10.50979995727539, 0, 32.484901428222656, 14.729700088500977, 0, 31.708599090576172, 16.56089973449707, 0, 30.573999404907227 ]); var teapotNormals = new Float32Array([ -0.9667419791221619, 0, -0.25575199723243713, -0.8930140137672424, 0.3698819875717163, -0.2563450038433075, -0.8934370279312134, 0.36910200119018555, 0.2559970021247864, -0.9668239951133728, 0, 0.2554430067539215, -0.0838799998164177, 0.03550700098276138, 0.9958429932594299, -0.09205400198698044, 0, 0.9957540035247803, 0.629721999168396, -0.2604379951953888, 0.7318620085716248, 0.6820489764213562, 0, 0.7313070297241211, 0.803725004196167, -0.3325839936733246, 0.4933690130710602, 0.8703010082244873, 0, 0.4925200045108795, -0.6834070086479187, 0.6834070086479187, -0.2567310035228729, -0.6835309863090515, 0.6835309863090515, 0.25606799125671387, -0.06492599844932556, 0.06492500007152557, 0.9957759976387024, 0.48139700293540955, -0.48139700293540955, 0.7324709892272949, 0.6148040294647217, -0.6148040294647217, 0.4939970076084137, -0.3698819875717163, 0.8930140137672424, -0.2563450038433075, -0.36910200119018555, 0.8934370279312134, 0.2559959888458252, -0.03550700098276138, 0.0838790014386177, 0.9958429932594299, 0.26043900847435, -0.6297230124473572, 0.7318609952926636, 0.3325839936733246, -0.803725004196167, 0.4933690130710602, -0.002848000032827258, 0.9661769866943359, -0.25786298513412476, -0.001921999966725707, 0.9670090079307556, 0.2547360062599182, -0.00026500000967644155, 0.09227199852466583, 0.9957339763641357, 0.00002300000051036477, -0.6820600032806396, 0.7312960028648376, 0, -0.8703010082244873, 0.4925200045108795, -0.002848000032827258, 0.9661769866943359, -0.25786298513412476, 0.37905800342559814, 0.852770984172821, -0.35929998755455017, 0.37711000442504883, 0.9140909910202026, 0.14908500015735626, -0.001921999966725707, 0.9670090079307556, 0.2547360062599182, 0.0275030005723238, 0.12255500257015228, 0.9920809864997864, -0.00026500000967644155, 0.09227199852466583, 0.9957339763641357, -0.26100900769233704, -0.6353650093078613, 0.7267630100250244, 0.00002300000051036477, -0.6820600032806396, 0.7312960028648376, -0.33248499035835266, -0.8042709827423096, 0.4925459921360016, 0, -0.8703010082244873, 0.4925200045108795, 0.6635469794273376, 0.6252639889717102, -0.4107919931411743, 0.712664008140564, 0.6976209878921509, 0.07372400164604187, 0.09972699731588364, 0.12198299914598465, 0.98750901222229, -0.4873189926147461, -0.4885669946670532, 0.7237560153007507, -0.6152420043945312, -0.6154839992523193, 0.4926010072231293, 0.8800280094146729, 0.3387089967727661, -0.3329069912433624, 0.9172769784927368, 0.36149299144744873, 0.16711199283599854, 0.11358699947595596, 0.04806999862194061, 0.9923650026321411, -0.6341490149497986, -0.2618879973888397, 0.7275090217590332, -0.8041260242462158, -0.33270499110221863, 0.49263399839401245, 0.9666900038719177, -0.010453999973833561, -0.2557379901409149, 0.967441976070404, -0.00810300000011921, 0.25296199321746826, 0.0934389978647232, -0.0012799999676644802, 0.9956240057945251, -0.6821659803390503, 0.0003429999924264848, 0.7311969995498657, -0.8703219890594482, 0.00005400000009103678, 0.492482990026474, 0.9666900038719177, -0.010453999973833561, -0.2557379901409149, 0.8930140137672424, -0.3698819875717163, -0.2563450038433075, 0.8934370279312134, -0.36910200119018555, 0.2559970021247864, 0.967441976070404, -0.00810300000011921, 0.25296199321746826, 0.0838799998164177, -0.03550700098276138, 0.9958429932594299, 0.0934389978647232, -0.0012799999676644802, 0.9956240057945251, -0.629721999168396, 0.2604379951953888, 0.7318620085716248, -0.6821659803390503, 0.0003429999924264848, 0.7311969995498657, -0.803725004196167, 0.3325839936733246, 0.4933690130710602, -0.8703219890594482, 0.00005400000009103678, 0.492482990026474, 0.6834070086479187, -0.6834070086479187, -0.2567310035228729, 0.6835309863090515, -0.6835309863090515, 0.25606799125671387, 0.06492599844932556, -0.06492500007152557, 0.9957759976387024, -0.48139700293540955, 0.48139700293540955, 0.7324709892272949, -0.6148040294647217, 0.6148040294647217, 0.4939970076084137, 0.3698819875717163, -0.8930140137672424, -0.2563450038433075, 0.36910200119018555, -0.8934370279312134, 0.2559959888458252, 0.03550700098276138, -0.0838790014386177, 0.9958429932594299, -0.26043900847435, 0.6297230124473572, 0.7318609952926636, -0.3325839936733246, 0.803725004196167, 0.4933690130710602, 0, -0.9667419791221619, -0.25575199723243713, 0, -0.9668239951133728, 0.2554430067539215, 0, -0.09205400198698044, 0.9957540035247803, 0, 0.6820489764213562, 0.7313070297241211, 0, 0.8703010082244873, 0.4925200045108795, 0, -0.9667419791221619, -0.25575199723243713, -0.3698819875717163, -0.8930140137672424, -0.2563450038433075, -0.36910200119018555, -0.8934370279312134, 0.2559970021247864, 0, -0.9668239951133728, 0.2554430067539215, -0.03550700098276138, -0.0838799998164177, 0.9958429932594299, 0, -0.09205400198698044, 0.9957540035247803, 0.2604379951953888, 0.629721999168396, 0.7318620085716248, 0, 0.6820489764213562, 0.7313070297241211, 0.3325839936733246, 0.803725004196167, 0.4933690130710602, 0, 0.8703010082244873, 0.4925200045108795, -0.6834070086479187, -0.6834070086479187, -0.2567310035228729, -0.6835309863090515, -0.6835309863090515, 0.25606799125671387, -0.06492500007152557, -0.06492599844932556, 0.9957759976387024, 0.48139700293540955, 0.48139700293540955, 0.7324709892272949, 0.6148040294647217, 0.6148040294647217, 0.4939970076084137, -0.8930140137672424, -0.3698819875717163, -0.2563450038433075, -0.8934370279312134, -0.36910200119018555, 0.2559959888458252, -0.0838790014386177, -0.03550700098276138, 0.9958429932594299, 0.6297230124473572, 0.26043900847435, 0.7318609952926636, 0.803725004196167, 0.3325839936733246, 0.4933690130710602, -0.9667419791221619, 0, -0.25575199723243713, -0.9668239951133728, 0, 0.2554430067539215, -0.09205400198698044, 0, 0.9957540035247803, 0.6820489764213562, 0, 0.7313070297241211, 0.8703010082244873, 0, 0.4925200045108795, 0.8703010082244873, 0, 0.4925200045108795, 0.803725004196167, -0.3325839936733246, 0.4933690130710602, 0.8454390168190002, -0.34983500838279724, 0.40354499220848083, 0.9153209924697876, 0, 0.4027250111103058, 0.8699960112571716, -0.36004599928855896, 0.33685898780822754, 0.9418079853057861, 0, 0.33615100383758545, 0.9041929841041565, -0.37428000569343567, 0.20579099655151367, 0.9786900281906128, 0, 0.20534199476242065, 0.9218789935112, -0.38175201416015625, -0.06636899709701538, 0.9978039860725403, 0, -0.06623899936676025, 0.6148040294647217, -0.6148040294647217, 0.4939970076084137, 0.6468020081520081, -0.6468020081520081, 0.40409600734710693, 0.6656550168991089, -0.6656550168991089, 0.3373520076274872, 0.6919230222702026, -0.6919230222702026, 0.20611999928951263, 0.7055429816246033, -0.7055429816246033, -0.06647899746894836, 0.3325839936733246, -0.803725004196167, 0.4933690130710602, 0.34983500838279724, -0.8454390168190002, 0.40354499220848083, 0.36004701256752014, -0.8699960112571716, 0.33685800433158875, 0.37428000569343567, -0.9041929841041565, 0.20579099655151367, 0.38175201416015625, -0.9218789935112, -0.06636899709701538, 0, -0.8703010082244873, 0.4925200045108795, 0, -0.9153209924697876, 0.4027250111103058, 0, -0.9418079853057861, 0.33615100383758545, 0, -0.9786900281906128, 0.20534199476242065, 0, -0.9978039860725403, -0.06623899936676025, 0, -0.8703010082244873, 0.4925200045108795, -0.33248499035835266, -0.8042709827423096, 0.4925459921360016, -0.34983500838279724, -0.8454390168190002, 0.40354499220848083, 0, -0.9153209924697876, 0.4027250111103058, -0.36004599928855896, -0.8699960112571716, 0.33685898780822754, 0, -0.9418079853057861, 0.33615100383758545, -0.37428000569343567, -0.9041929841041565, 0.20579099655151367, 0, -0.9786900281906128, 0.20534199476242065, -0.38175201416015625, -0.9218789935112, -0.06636899709701538, 0, -0.9978039860725403, -0.06623899936676025, -0.6152420043945312, -0.6154839992523193, 0.4926010072231293, -0.6468020081520081, -0.6468020081520081, 0.40409600734710693, -0.6656550168991089, -0.6656550168991089, 0.3373520076274872, -0.6919230222702026, -0.6919230222702026, 0.20611999928951263, -0.7055429816246033, -0.7055429816246033, -0.06647899746894836, -0.8041260242462158, -0.33270499110221863, 0.49263399839401245, -0.8454390168190002, -0.34983500838279724, 0.40354499220848083, -0.8699960112571716, -0.36004701256752014, 0.33685800433158875, -0.9041929841041565, -0.37428000569343567, 0.20579099655151367, -0.9218789935112, -0.38175201416015625, -0.06636899709701538, -0.8703219890594482, 0.00005400000009103678, 0.492482990026474, -0.9153209924697876, 0, 0.4027250111103058, -0.9418079853057861, 0, 0.33615100383758545, -0.9786900281906128, 0, 0.20534199476242065, -0.9978039860725403, 0, -0.06623899936676025, -0.8703219890594482, 0.00005400000009103678, 0.492482990026474, -0.803725004196167, 0.3325839936733246, 0.4933690130710602, -0.8454390168190002, 0.34983500838279724, 0.40354499220848083, -0.9153209924697876, 0, 0.4027250111103058, -0.8699960112571716, 0.36004599928855896, 0.33685898780822754, -0.9418079853057861, 0, 0.33615100383758545, -0.9041929841041565, 0.37428000569343567, 0.20579099655151367, -0.9786900281906128, 0, 0.20534199476242065, -0.9218789935112, 0.38175201416015625, -0.06636899709701538, -0.9978039860725403, 0, -0.06623899936676025, -0.6148040294647217, 0.6148040294647217, 0.4939970076084137, -0.6468020081520081, 0.6468020081520081, 0.40409600734710693, -0.6656550168991089, 0.6656550168991089, 0.3373520076274872, -0.6919230222702026, 0.6919230222702026, 0.20611999928951263, -0.7055429816246033, 0.7055429816246033, -0.06647899746894836, -0.3325839936733246, 0.803725004196167, 0.4933690130710602, -0.34983500838279724, 0.8454390168190002, 0.40354499220848083, -0.36004701256752014, 0.8699960112571716, 0.33685800433158875, -0.37428000569343567, 0.9041929841041565, 0.20579099655151367, -0.38175201416015625, 0.9218789935112, -0.06636899709701538, 0, 0.8703010082244873, 0.4925200045108795, 0, 0.9153209924697876, 0.4027250111103058, 0, 0.9418079853057861, 0.33615100383758545, 0, 0.9786900281906128, 0.20534199476242065, 0, 0.9978039860725403, -0.06623899936676025, 0, 0.8703010082244873, 0.4925200045108795, 0.3325839936733246, 0.803725004196167, 0.4933690130710602, 0.34983500838279724, 0.8454390168190002, 0.40354499220848083, 0, 0.9153209924697876, 0.4027250111103058, 0.36004599928855896, 0.8699960112571716, 0.33685898780822754, 0, 0.9418079853057861, 0.33615100383758545, 0.37428000569343567, 0.9041929841041565, 0.20579099655151367, 0, 0.9786900281906128, 0.20534199476242065, 0.38175201416015625, 0.9218789935112, -0.06636899709701538, 0, 0.9978039860725403, -0.06623899936676025, 0.6148040294647217, 0.6148040294647217, 0.4939970076084137, 0.6468020081520081, 0.6468020081520081, 0.40409600734710693, 0.6656550168991089, 0.6656550168991089, 0.3373520076274872, 0.6919230222702026, 0.6919230222702026, 0.20611999928951263, 0.7055429816246033, 0.7055429816246033, -0.06647899746894836, 0.803725004196167, 0.3325839936733246, 0.4933690130710602, 0.8454390168190002, 0.34983500838279724, 0.40354499220848083, 0.8699960112571716, 0.36004701256752014, 0.33685800433158875, 0.9041929841041565, 0.37428000569343567, 0.20579099655151367, 0.9218789935112, 0.38175201416015625, -0.06636899709701538, 0.8703010082244873, 0, 0.4925200045108795, 0.9153209924697876, 0, 0.4027250111103058, 0.9418079853057861, 0, 0.33615100383758545, 0.9786900281906128, 0, 0.20534199476242065, 0.9978039860725403, 0, -0.06623899936676025, 0.9978039860725403, 0, -0.06623899936676025, 0.9218789935112, -0.38175201416015625, -0.06636899709701538, 0.8314369916915894, -0.3441790044307709, -0.4361799955368042, 0.9001820087432861, 0, -0.4355129897594452, 0.6735119819641113, -0.2785939872264862, -0.6846650242805481, 0.7296109795570374, 0, -0.6838629841804504, 0.6403989791870117, -0.26487401127815247, -0.7209240198135376, 0.6939510107040405, 0, -0.7200220227241516, 0.7329490184783936, -0.303166002035141, -0.6089959740638733, 0.7939500212669373, 0, -0.6079840064048767, 0.7055429816246033, -0.7055429816246033, -0.06647899746894836, 0.6360920071601868, -0.6360920071601868, -0.4367780089378357, 0.5149649977684021, -0.5149649977684021, -0.6852890253067017, 0.48965099453926086, -0.48965099453926086, -0.7214459776878357, 0.5605549812316895, -0.5605549812316895, -0.6095539927482605, 0.38175201416015625, -0.9218789935112, -0.06636899709701538, 0.3441790044307709, -0.8314369916915894, -0.4361799955368042, 0.2785939872264862, -0.6735119819641113, -0.6846650242805481, 0.26487401127815247, -0.6403989791870117, -0.7209240198135376, 0.303166002035141, -0.7329490184783936, -0.6089959740638733, 0, -0.9978039860725403, -0.06623899936676025, 0, -0.9001820087432861, -0.4355129897594452, 0, -0.7296109795570374, -0.6838629841804504, 0, -0.6939510107040405, -0.7200220227241516, 0, -0.7939500212669373, -0.6079840064048767, 0, -0.9978039860725403, -0.06623899936676025, -0.38175201416015625, -0.9218789935112, -0.06636899709701538, -0.3441790044307709, -0.8314369916915894, -0.4361799955368042, 0, -0.9001820087432861, -0.4355129897594452, -0.2785939872264862, -0.6735119819641113, -0.6846650242805481, 0, -0.7296109795570374, -0.6838629841804504, -0.26487401127815247, -0.6403989791870117, -0.7209240198135376, 0, -0.6939510107040405, -0.7200220227241516, -0.303166002035141, -0.7329490184783936, -0.6089959740638733, 0, -0.7939500212669373, -0.6079840064048767, -0.7055429816246033, -0.7055429816246033, -0.06647899746894836, -0.6360920071601868, -0.6360920071601868, -0.4367780089378357, -0.5149649977684021, -0.5149649977684021, -0.6852890253067017, -0.48965099453926086, -0.48965099453926086, -0.7214459776878357, -0.5605549812316895, -0.5605549812316895, -0.6095539927482605, -0.9218789935112, -0.38175201416015625, -0.06636899709701538, -0.8314369916915894, -0.3441790044307709, -0.4361799955368042, -0.6735119819641113, -0.2785939872264862, -0.6846650242805481, -0.6403989791870117, -0.26487401127815247, -0.7209240198135376, -0.7329490184783936, -0.303166002035141, -0.6089959740638733, -0.9978039860725403, 0, -0.06623899936676025, -0.9001820087432861, 0, -0.4355129897594452, -0.7296109795570374, 0, -0.6838629841804504, -0.6939510107040405, 0, -0.7200220227241516, -0.7939500212669373, 0, -0.6079840064048767, -0.9978039860725403, 0, -0.06623899936676025, -0.9218789935112, 0.38175201416015625, -0.06636899709701538, -0.8314369916915894, 0.3441790044307709, -0.4361799955368042, -0.9001820087432861, 0, -0.4355129897594452, -0.6735119819641113, 0.2785939872264862, -0.6846650242805481, -0.7296109795570374, 0, -0.6838629841804504, -0.6403989791870117, 0.26487401127815247, -0.7209240198135376, -0.6939510107040405, 0, -0.7200220227241516, -0.7329490184783936, 0.303166002035141, -0.6089959740638733, -0.7939500212669373, 0, -0.6079840064048767, -0.7055429816246033, 0.7055429816246033, -0.06647899746894836, -0.6360920071601868, 0.6360920071601868, -0.4367780089378357, -0.5149649977684021, 0.5149649977684021, -0.6852890253067017, -0.48965099453926086, 0.48965099453926086, -0.7214459776878357, -0.5605549812316895, 0.5605549812316895, -0.6095539927482605, -0.38175201416015625, 0.9218789935112, -0.06636899709701538, -0.3441790044307709, 0.8314369916915894, -0.4361799955368042, -0.2785939872264862, 0.6735119819641113, -0.6846650242805481, -0.26487401127815247, 0.6403989791870117, -0.7209240198135376, -0.303166002035141, 0.7329490184783936, -0.6089959740638733, 0, 0.9978039860725403, -0.06623899936676025, 0, 0.9001820087432861, -0.4355129897594452, 0, 0.7296109795570374, -0.6838629841804504, 0, 0.6939510107040405, -0.7200220227241516, 0, 0.7939500212669373, -0.6079840064048767, 0, 0.9978039860725403, -0.06623899936676025, 0.38175201416015625, 0.9218789935112, -0.06636899709701538, 0.3441790044307709, 0.8314369916915894, -0.4361799955368042, 0, 0.9001820087432861, -0.4355129897594452, 0.2785939872264862, 0.6735119819641113, -0.6846650242805481, 0, 0.7296109795570374, -0.6838629841804504, 0.26487401127815247, 0.6403989791870117, -0.7209240198135376, 0, 0.6939510107040405, -0.7200220227241516, 0.303166002035141, 0.7329490184783936, -0.6089959740638733, 0, 0.7939500212669373, -0.6079840064048767, 0.7055429816246033, 0.7055429816246033, -0.06647899746894836, 0.6360920071601868, 0.6360920071601868, -0.4367780089378357, 0.5149649977684021, 0.5149649977684021, -0.6852890253067017, 0.48965099453926086, 0.48965099453926086, -0.7214459776878357, 0.5605549812316895, 0.5605549812316895, -0.6095539927482605, 0.9218789935112, 0.38175201416015625, -0.06636899709701538, 0.8314369916915894, 0.3441790044307709, -0.4361799955368042, 0.6735119819641113, 0.2785939872264862, -0.6846650242805481, 0.6403989791870117, 0.26487401127815247, -0.7209240198135376, 0.7329490184783936, 0.303166002035141, -0.6089959740638733, 0.9978039860725403, 0, -0.06623899936676025, 0.9001820087432861, 0, -0.4355129897594452, 0.7296109795570374, 0, -0.6838629841804504, 0.6939510107040405, 0, -0.7200220227241516, 0.7939500212669373, 0, -0.6079840064048767, 0.7939500212669373, 0, -0.6079840064048767, 0.7329490184783936, -0.303166002035141, -0.6089959740638733, 0.576229989528656, -0.23821599781513214, -0.7818009853363037, 0.6238600015640259, 0, -0.7815359830856323, 0.16362899541854858, -0.06752700358629227, -0.9842079877853394, 0.17729100584983826, 0, -0.984158992767334, 0.04542100057005882, -0.018735000863671303, -0.9987919926643372, 0.04920699819922447, 0, -0.9987890124320984, 0, 0, -1, 0, 0, -1, 0.5605549812316895, -0.5605549812316895, -0.6095539927482605, 0.44041600823402405, -0.44041600823402405, -0.7823479771614075, 0.12490200251340866, -0.12490200251340866, -0.9842759966850281, 0.034662000834941864, -0.034662000834941864, -0.9987980127334595, 0, 0, -1, 0.303166002035141, -0.7329490184783936, -0.6089959740638733, 0.23821599781513214, -0.576229989528656, -0.7818009853363037, 0.06752700358629227, -0.16362899541854858, -0.9842079877853394, 0.018735000863671303, -0.04542100057005882, -0.9987919926643372, 0, 0, -1, 0, -0.7939500212669373, -0.6079840064048767, 0, -0.6238600015640259, -0.7815359830856323, 0, -0.17729100584983826, -0.984158992767334, 0, -0.04920699819922447, -0.9987890124320984, 0, 0, -1, 0, -0.7939500212669373, -0.6079840064048767, -0.303166002035141, -0.7329490184783936, -0.6089959740638733, -0.23821599781513214, -0.576229989528656, -0.7818009853363037, 0, -0.6238600015640259, -0.7815359830856323, -0.06752700358629227, -0.16362899541854858, -0.9842079877853394, 0, -0.17729100584983826, -0.984158992767334, -0.018735000863671303, -0.04542100057005882, -0.9987919926643372, 0, -0.04920699819922447, -0.9987890124320984, 0, 0, -1, 0, 0, -1, -0.5605549812316895, -0.5605549812316895, -0.6095539927482605, -0.44041600823402405, -0.44041600823402405, -0.7823479771614075, -0.12490200251340866, -0.12490200251340866, -0.9842759966850281, -0.034662000834941864, -0.034662000834941864, -0.9987980127334595, 0, 0, -1, -0.7329490184783936, -0.303166002035141, -0.6089959740638733, -0.576229989528656, -0.23821599781513214, -0.7818009853363037, -0.16362899541854858, -0.06752700358629227, -0.9842079877853394, -0.04542100057005882, -0.018735000863671303, -0.9987919926643372, 0, 0, -1, -0.7939500212669373, 0, -0.6079840064048767, -0.6238600015640259, 0, -0.7815359830856323, -0.17729100584983826, 0, -0.984158992767334, -0.04920699819922447, 0, -0.9987890124320984, 0, 0, -1, -0.7939500212669373, 0, -0.6079840064048767, -0.7329490184783936, 0.303166002035141, -0.6089959740638733, -0.576229989528656, 0.23821599781513214, -0.7818009853363037, -0.6238600015640259, 0, -0.7815359830856323, -0.16362899541854858, 0.06752700358629227, -0.9842079877853394, -0.17729100584983826, 0, -0.984158992767334, -0.04542100057005882, 0.018735000863671303, -0.9987919926643372, -0.04920699819922447, 0, -0.9987890124320984, 0, 0, -1, 0, 0, -1, -0.5605549812316895, 0.5605549812316895, -0.6095539927482605, -0.44041600823402405, 0.44041600823402405, -0.7823479771614075, -0.12490200251340866, 0.12490200251340866, -0.9842759966850281, -0.034662000834941864, 0.034662000834941864, -0.9987980127334595, 0, 0, -1, -0.303166002035141, 0.7329490184783936, -0.6089959740638733, -0.23821599781513214, 0.576229989528656, -0.7818009853363037, -0.06752700358629227, 0.16362899541854858, -0.9842079877853394, -0.018735000863671303, 0.04542100057005882, -0.9987919926643372, 0, 0, -1, 0, 0.7939500212669373, -0.6079840064048767, 0, 0.6238600015640259, -0.7815359830856323, 0, 0.17729100584983826, -0.984158992767334, 0, 0.04920699819922447, -0.9987890124320984, 0, 0, -1, 0, 0.7939500212669373, -0.6079840064048767, 0.303166002035141, 0.7329490184783936, -0.6089959740638733, 0.23821599781513214, 0.576229989528656, -0.7818009853363037, 0, 0.6238600015640259, -0.7815359830856323, 0.06752700358629227, 0.16362899541854858, -0.9842079877853394, 0, 0.17729100584983826, -0.984158992767334, 0.018735000863671303, 0.04542100057005882, -0.9987919926643372, 0, 0.04920699819922447, -0.9987890124320984, 0, 0, -1, 0, 0, -1, 0.5605549812316895, 0.5605549812316895, -0.6095539927482605, 0.44041600823402405, 0.44041600823402405, -0.7823479771614075, 0.12490200251340866, 0.12490200251340866, -0.9842759966850281, 0.034662000834941864, 0.034662000834941864, -0.9987980127334595, 0, 0, -1, 0.7329490184783936, 0.303166002035141, -0.6089959740638733, 0.576229989528656, 0.23821599781513214, -0.7818009853363037, 0.16362899541854858, 0.06752700358629227, -0.9842079877853394, 0.04542100057005882, 0.018735000863671303, -0.9987919926643372, 0, 0, -1, 0.7939500212669373, 0, -0.6079840064048767, 0.6238600015640259, 0, -0.7815359830856323, 0.17729100584983826, 0, -0.984158992767334, 0.04920699819922447, 0, -0.9987890124320984, 0, 0, -1, 0.007784999907016754, 0.00021499999274965376, -0.999970018863678, 0.007038000039756298, -0.5829259753227234, -0.8124949932098389, 0.0361270010471344, -0.5456140041351318, -0.837257981300354, 0.03913800045847893, 0.0009879999561235309, -0.9992330074310303, 0.16184599697589874, -0.5630490183830261, -0.8104209899902344, 0.17951199412345886, 0.0043680001981556416, -0.9837459921836853, 0.4823650121688843, -0.6427459716796875, -0.5951480269432068, 0.6122999787330627, 0.010459000244736671, -0.790556013584137, 0.7387199997901917, -0.6641989946365356, -0.11459299921989441, 0.9861519932746887, 0.006668999791145325, -0.16570700705051422, -0.0019079999765381217, -0.9867690205574036, 0.1621209979057312, 0.002761000068858266, -0.9998499751091003, 0.017105000093579292, 0.010532000102102757, -0.9972469806671143, 0.07339800149202347, -0.06604000180959702, -0.9893029928207397, 0.13006900250911713, -0.09442699700593948, -0.9953929781913757, 0.016594000160694122, -0.009201999753713608, -0.4902929961681366, 0.8715090155601501, -0.04860600084066391, -0.5394579768180847, 0.8406090140342712, -0.22329799830913544, -0.5527390241622925, 0.8028810024261475, -0.5963649749755859, -0.5751349925994873, 0.5599709749221802, -0.8033369779586792, -0.5916029810905457, 0.06823500245809555, -0.01056000031530857, -0.00010299999848939478, 0.9999439716339111, -0.05879800021648407, -0.0007089999853633344, 0.9982699751853943, -0.28071001172065735, -0.0032679999712854624, 0.9597870111465454, -0.7497230172157288, -0.004267000127583742, 0.6617379784584045, -0.9973509907722473, -0.0020580000709742308, 0.07271400094032288, -0.01056000031530857, -0.00010299999848939478, 0.9999439716339111, -0.008791999891400337, 0.49032899737358093, 0.8714929819107056, -0.04649300128221512, 0.5387560129165649, 0.8411779999732971, -0.05879800021648407, -0.0007089999853633344, 0.9982699751853943, -0.21790899336338043, 0.5491610169410706, 0.8068069815635681, -0.28071001172065735, -0.0032679999712854624, 0.9597870111465454, -0.5972909927368164, 0.5741199851036072, 0.560027003288269, -0.7497230172157288, -0.004267000127583742, 0.6617379784584045, -0.8040000200271606, 0.5912910103797913, 0.0629120022058487, -0.9973509907722473, -0.0020580000709742308, 0.07271400094032288, -0.0018050000071525574, 0.986840009689331, 0.16169099509716034, 0.0020310000982135534, 0.999891996383667, 0.014553000219166279, 0.009215000085532665, 0.9981520175933838, 0.060068998485803604, -0.059335000813007355, 0.9917230010032654, 0.11386600136756897, -0.08690100163221359, 0.9961410164833069, 0.01228999998420477, 0.006417000200599432, 0.5830950140953064, -0.812379002571106, 0.03378299996256828, 0.5453730225563049, -0.8375130295753479, 0.1571130007505417, 0.562188982963562, -0.8119469881057739, 0.4844059944152832, 0.6465290188789368, -0.5893650054931641, 0.7388700246810913, 0.6661880016326904, -0.10131999850273132, 0.007784999907016754, 0.00021499999274965376, -0.999970018863678, 0.03913800045847893, 0.0009879999561235309, -0.9992330074310303, 0.17951199412345886, 0.0043680001981556416, -0.9837459921836853, 0.6122999787330627, 0.010459000244736671, -0.790556013584137, 0.9861519932746887, 0.006668999791145325, -0.16570700705051422, 0.9861519932746887, 0.006668999791145325, -0.16570700705051422, 0.7387199997901917, -0.6641989946365356, -0.11459299921989441, 0.7256090044975281, -0.6373609900474548, 0.25935098528862, 0.94651198387146, 0.0033569999504834414, 0.3226499855518341, 0.6459450125694275, -0.6077200174331665, 0.46198800206184387, 0.8258299827575684, 0.007451999932527542, 0.5638700127601624, 0.5316150188446045, -0.5586140155792236, 0.6366599798202515, 0.6500110030174255, 0.006936000194400549, 0.759893000125885, 0.4249640107154846, -0.5955389738082886, 0.6817179918289185, 0.5324289798736572, 0.005243999883532524, 0.8464580178260803, -0.09442699700593948, -0.9953929781913757, 0.016594000160694122, -0.04956100136041641, -0.9985759854316711, -0.01975500024855137, -0.03781700134277344, -0.998649001121521, -0.035624999552965164, -0.0379129983484745, -0.9986140131950378, -0.03651199862360954, -0.1688539981842041, -0.9395300149917603, -0.2979460060596466, -0.8033369779586792, -0.5916029810905457, 0.06823500245809555, -0.7423409819602966, -0.5995240211486816, -0.2991659939289093, -0.6196020245552063, -0.5795029997825623, -0.5294060111045837, -0.483707994222641, -0.5438370108604431, -0.6857600212097168, -0.44529199600219727, -0.4131770133972168, -0.7943549752235413, -0.9973509907722473, -0.0020580000709742308, 0.07271400094032288, -0.9265130162239075, -0.0019950000569224358, -0.3762570023536682, -0.7539200186729431, -0.004317000042647123, -0.6569520235061646, -0.5662239789962769, -0.003461000043898821, -0.8242440223693848, -0.4818040132522583, -0.0018500000005587935, -0.8762770295143127, -0.9973509907722473, -0.0020580000709742308, 0.07271400094032288, -0.8040000200271606, 0.5912910103797913, 0.0629120022058487, -0.7446749806404114, 0.5989770293235779, -0.29442399740219116, -0.9265130162239075, -0.0019950000569224358, -0.3762570023536682, -0.6219490170478821, 0.5781649947166443, -0.5281140208244324, -0.7539200186729431, -0.004317000042647123, -0.6569520235061646, -0.48117101192474365, 0.5428280234336853, -0.6883400082588196, -0.5662239789962769, -0.003461000043898821, -0.8242440223693848, -0.43805500864982605, 0.41574400663375854, -0.7970349788665771, -0.4818040132522583, -0.0018500000005587935, -0.8762770295143127, -0.08690100163221359, 0.9961410164833069, 0.01228999998420477, -0.04433799907565117, 0.9988710284233093, -0.017055999487638474, -0.026177000254392624, 0.9992600083351135, -0.02816700004041195, -0.025293000042438507, 0.9992780089378357, -0.028332000598311424, -0.15748199820518494, 0.9441670179367065, -0.28939300775527954, 0.7388700246810913, 0.6661880016326904, -0.10131999850273132, 0.7282440066337585, 0.63714200258255, 0.25240999460220337, 0.6470540165901184, 0.6082550287246704, 0.4597249925136566, 0.5229939818382263, 0.5621700286865234, 0.6406570076942444, 0.4099780023097992, 0.6046689748764038, 0.6828569769859314, 0.9861519932746887, 0.006668999791145325, -0.16570700705051422, 0.94651198387146, 0.0033569999504834414, 0.3226499855518341, 0.8258299827575684, 0.007451999932527542, 0.5638700127601624, 0.6500110030174255, 0.006936000194400549, 0.759893000125885, 0.5324289798736572, 0.005243999883532524, 0.8464580178260803, -0.230786994099617, 0.006523000076413155, 0.9729819893836975, -0.15287800133228302, -0.7101899981498718, 0.6872109770774841, -0.31672099232673645, -0.7021129727363586, 0.6377500295639038, -0.5489360094070435, 0.0015109999803826213, 0.8358629941940308, -0.6010670065879822, -0.645330011844635, 0.471451997756958, -0.8756710290908813, -0.009891999885439873, 0.4828070104122162, -0.635890007019043, -0.629800021648407, 0.4460900127887726, -0.8775539994239807, -0.01909100078046322, 0.47909700870513916, -0.4357450008392334, -0.670009970664978, 0.6010090112686157, -0.6961889863014221, -0.02449600026011467, 0.7174400091171265, 0.11111299693584442, -0.9901599884033203, -0.08506900072097778, 0.22330999374389648, -0.9747260212898254, 0.006539999973028898, 0.19009700417518616, -0.9694579839706421, 0.15496399998664856, 0.005270000081509352, -0.9818699955940247, 0.18948200345039368, -0.011750999838113785, -0.9690240025520325, 0.24668699502944946, 0.3439059853553772, -0.5994120240211487, -0.7227950096130371, 0.5724899768829346, -0.5916270017623901, -0.5676559805870056, 0.7874360084533691, -0.5605109930038452, -0.2564600110054016, 0.6470969915390015, -0.6981409788131714, -0.3063740134239197, 0.4275279939174652, -0.7535750269889832, -0.49934399127960205, 0.4109260141849518, -0.0012839999981224537, -0.9116680026054382, 0.6715199947357178, 0.0008989999769255519, -0.7409859895706177, 0.9220259785652161, 0.00725199980661273, -0.3870599865913391, 0.8469099998474121, 0.01385399978607893, -0.5315560102462769, 0.5359240174293518, 0.010503999888896942, -0.8442010283470154, 0.4109260141849518, -0.0012839999981224537, -0.9116680026054382, 0.3411880135536194, 0.6009309887886047, -0.7228230237960815, 0.5786640048027039, 0.591838002204895, -0.5611389875411987, 0.6715199947357178, 0.0008989999769255519, -0.7409859895706177, 0.7848690152168274, 0.5665420293807983, -0.25102001428604126, 0.9220259785652161, 0.00725199980661273, -0.3870599865913391, 0.6426810026168823, 0.7039899826049805, -0.3022570013999939, 0.8469099998474121, 0.01385399978607893, -0.5315560102462769, 0.4185889959335327, 0.7581170201301575, -0.5000420212745667, 0.5359240174293518, 0.010503999888896942, -0.8442010283470154, 0.11580599844455719, 0.9901139736175537, -0.07913900166749954, 0.23281100392341614, 0.9724410176277161, 0.012564999982714653, 0.20666299760341644, 0.9662799835205078, 0.15360000729560852, 0.02449899911880493, 0.9865779876708984, 0.16144299507141113, 0.0033809999004006386, 0.9774550199508667, 0.2111150026321411, -0.13491199910640717, 0.7135509848594666, 0.6874909996986389, -0.31953999400138855, 0.7050619721412659, 0.6330729722976685, -0.6039019823074341, 0.6499029994010925, 0.4614419937133789, -0.6318150162696838, 0.6400719881057739, 0.43716898560523987, -0.4243049919605255, 0.6667500138282776, 0.6127070188522339, -0.230786994099617, 0.006523000076413155, 0.9729819893836975, -0.5489360094070435, 0.0015109999803826213, 0.8358629941940308, -0.8756710290908813, -0.009891999885439873, 0.4828070104122162, -0.8775539994239807, -0.01909100078046322, 0.47909700870513916, -0.6961889863014221, -0.02449600026011467, 0.7174400091171265, -0.6961889863014221, -0.02449600026011467, 0.7174400091171265, -0.4357450008392334, -0.670009970664978, 0.6010090112686157, -0.25985801219940186, -0.5525479912757874, 0.7919380068778992, -0.42579901218414307, -0.010804999619722366, 0.9047530293464661, 0.009537000209093094, 0.021669000387191772, 0.9997199773788452, 0.022041000425815582, -0.001623000018298626, 0.9997559785842896, 0.4101540148258209, 0.8490809798240662, 0.3329179883003235, 0.9995980262756348, -0.01155600044876337, 0.02587899938225746, 0.5415220260620117, 0.6370009779930115, -0.5486199855804443, 0.7095860242843628, -0.009670999832451344, -0.7045519948005676, -0.011750999838113785, -0.9690240025520325, 0.24668699502944946, 0.046310000121593475, -0.8891720175743103, 0.45522499084472656, -0.010688000358641148, -0.14889900386333466, 0.9887949824333191, -0.04437499865889549, 0.7291200160980225, 0.6829460263252258, 0.12282499670982361, 0.9923850297927856, 0.009232000447809696, 0.4275279939174652, -0.7535750269889832, -0.49934399127960205, 0.48183900117874146, -0.857479989528656, -0.18044300377368927, 0.45527198910713196, -0.49992498755455017, 0.7367510199546814, -0.22054199874401093, 0.3582780063152313, 0.9071930050849915, -0.23591899871826172, 0.7157959938049316, 0.6572499871253967, 0.5359240174293518, 0.010503999888896942, -0.8442010283470154, 0.7280910015106201, 0.015584999695420265, -0.6853029727935791, 0.8887389898300171, 0.016679000109434128, 0.4581089913845062, -0.26009801030158997, -0.0007999999797903001, 0.965582013130188, -0.37161099910736084, 0.004416999872773886, 0.9283779859542847, 0.5359240174293518, 0.010503999888896942, -0.8442010283470154, 0.4185889959335327, 0.7581170201301575, -0.5000420212745667, 0.4801650047302246, 0.8588529825210571, -0.17836299538612366, 0.7280910015106201, 0.015584999695420265, -0.6853029727935791, 0.4881030023097992, 0.49794700741767883, 0.7168020009994507, 0.8887389898300171, 0.016679000109434128, 0.4581089913845062, -0.2220049947500229, -0.36189401149749756, 0.9053990244865417, -0.26009801030158997, -0.0007999999797903001, 0.965582013130188, -0.23540399968624115, -0.7104769945144653, 0.6631799936294556, -0.37161099910736084, 0.004416999872773886, 0.9283779859542847, 0.0033809999004006386, 0.9774550199508667, 0.2111150026321411, 0.058719001710414886, 0.8971999883651733, 0.437703013420105, 0.0013249999610707164, 0.164000004529953, 0.9864590167999268, -0.04418899863958359, -0.7303190231323242, 0.6816750168800354, 0.13880200684070587, -0.9897300004959106, -0.034189000725746155, -0.4243049919605255, 0.6667500138282776, 0.6127070188522339, -0.25888898968696594, 0.5453789830207825, 0.7972059845924377, 0.012268000282347202, -0.01928500086069107, 0.9997389912605286, 0.3986299932003021, -0.8456630110740662, 0.3548929989337921, 0.5375639796257019, -0.6107370257377625, -0.5813990235328674, -0.6961889863014221, -0.02449600026011467, 0.7174400091171265, -0.42579901218414307, -0.010804999619722366, 0.9047530293464661, 0.022041000425815582, -0.001623000018298626, 0.9997559785842896, 0.9995980262756348, -0.01155600044876337, 0.02587899938225746, 0.7095860242843628, -0.009670999832451344, -0.7045519948005676, 0, 0, 1, 0, 0, 1, 0.7626410126686096, -0.31482499837875366, 0.5650339722633362, 0.8245400190353394, -0.00001700000029813964, 0.5658029913902283, 0.8479819893836975, -0.3500339984893799, -0.39799800515174866, 0.917701005935669, -0.00003300000025774352, -0.397271990776062, 0.8641409873962402, -0.35644200444221497, -0.3552600145339966, 0.9352689981460571, -0.00011200000153621659, -0.3539389967918396, 0.7209920287132263, -0.29793301224708557, 0.6256250143051147, 0.7807120084762573, -0.00007500000356230885, 0.6248909831047058, 0, 0, 1, 0.5833569765090942, -0.5833380222320557, 0.5651649832725525, 0.648485004901886, -0.6484479904174805, -0.3987259864807129, 0.6608719825744629, -0.6607480049133301, -0.35589399933815, 0.5518630146980286, -0.5517799854278564, 0.6252880096435547, 0, 0, 1, 0.31482499837875366, -0.762628972530365, 0.5650510191917419, 0.35004499554634094, -0.8479880094528198, -0.39797601103782654, 0.35647401213645935, -0.8641520142555237, -0.35519900918006897, 0.29798200726509094, -0.7210670113563538, 0.6255149841308594, 0, 0, 1, -0.00001700000029813964, -0.8245400190353394, 0.5658029913902283, -0.00003300000025774352, -0.917701005935669, -0.397271990776062, -0.00011200000153621659, -0.9352689981460571, -0.3539389967918396, -0.00007500000356230885, -0.7807120084762573, 0.6248900294303894, 0, 0, 1, 0, 0, 1, -0.31482499837875366, -0.7626410126686096, 0.5650339722633362, -0.00001700000029813964, -0.8245400190353394, 0.5658029913902283, -0.3500339984893799, -0.8479819893836975, -0.39799800515174866, -0.00003300000025774352, -0.917701005935669, -0.397271990776062, -0.35644200444221497, -0.8641409873962402, -0.3552600145339966, -0.00011200000153621659, -0.9352689981460571, -0.3539389967918396, -0.29793301224708557, -0.7209920287132263, 0.6256250143051147, -0.00007500000356230885, -0.7807120084762573, 0.6248900294303894, 0, 0, 1, -0.5833380222320557, -0.5833569765090942, 0.5651649832725525, -0.6484479904174805, -0.648485004901886, -0.3987259864807129, -0.6607480049133301, -0.6608719825744629, -0.35589399933815, -0.5517799854278564, -0.5518630146980286, 0.6252880096435547, 0, 0, 1, -0.762628972530365, -0.31482499837875366, 0.5650510191917419, -0.8479880094528198, -0.35004499554634094, -0.39797601103782654, -0.8641520142555237, -0.35647401213645935, -0.35519900918006897, -0.7210670113563538, -0.29798200726509094, 0.6255149841308594, 0, 0, 1, -0.8245400190353394, 0.00001700000029813964, 0.5658029913902283, -0.917701005935669, 0.00003300000025774352, -0.397271990776062, -0.9352689981460571, 0.00011200000153621659, -0.3539389967918396, -0.7807120084762573, 0.00007500000356230885, 0.6248900294303894, 0, 0, 1, 0, 0, 1, -0.7626410126686096, 0.31482499837875366, 0.5650339722633362, -0.8245400190353394, 0.00001700000029813964, 0.5658029913902283, -0.8479819893836975, 0.3500339984893799, -0.39799800515174866, -0.917701005935669, 0.00003300000025774352, -0.397271990776062, -0.8641409873962402, 0.35644200444221497, -0.3552600145339966, -0.9352689981460571, 0.00011200000153621659, -0.3539389967918396, -0.7209920287132263, 0.29793301224708557, 0.6256250143051147, -0.7807120084762573, 0.00007500000356230885, 0.6248900294303894, 0, 0, 1, -0.5833569765090942, 0.5833380222320557, 0.5651649832725525, -0.648485004901886, 0.6484479904174805, -0.3987259864807129, -0.6608719825744629, 0.6607480049133301, -0.35589399933815, -0.5518630146980286, 0.5517799854278564, 0.6252880096435547, 0, 0, 1, -0.31482499837875366, 0.762628972530365, 0.5650510191917419, -0.35004499554634094, 0.8479880094528198, -0.39797601103782654, -0.35647401213645935, 0.8641520142555237, -0.35519900918006897, -0.29798200726509094, 0.7210670113563538, 0.6255149841308594, 0, 0, 1, 0.00001700000029813964, 0.8245400190353394, 0.5658029913902283, 0.00003300000025774352, 0.917701005935669, -0.397271990776062, 0.00011200000153621659, 0.9352689981460571, -0.3539389967918396, 0.00007500000356230885, 0.7807120084762573, 0.6248900294303894, 0, 0, 1, 0, 0, 1, 0.31482499837875366, 0.7626410126686096, 0.5650339722633362, 0.00001700000029813964, 0.8245400190353394, 0.5658029913902283, 0.3500339984893799, 0.8479819893836975, -0.39799800515174866, 0.00003300000025774352, 0.917701005935669, -0.397271990776062, 0.35644200444221497, 0.8641409873962402, -0.3552600145339966, 0.00011200000153621659, 0.9352689981460571, -0.3539389967918396, 0.29793301224708557, 0.7209920287132263, 0.6256250143051147, 0.00007500000356230885, 0.7807120084762573, 0.6248900294303894, 0, 0, 1, 0.5833380222320557, 0.5833569765090942, 0.5651649832725525, 0.6484479904174805, 0.648485004901886, -0.3987259864807129, 0.6607480049133301, 0.6608719825744629, -0.35589399933815, 0.5517799854278564, 0.5518630146980286, 0.6252880096435547, 0, 0, 1, 0.762628972530365, 0.31482499837875366, 0.5650510191917419, 0.8479880094528198, 0.35004499554634094, -0.39797601103782654, 0.8641520142555237, 0.35647401213645935, -0.35519900918006897, 0.7210670113563538, 0.29798200726509094, 0.6255149841308594, 0, 0, 1, 0.8245400190353394, -0.00001700000029813964, 0.5658029913902283, 0.917701005935669, -0.00003300000025774352, -0.397271990776062, 0.9352689981460571, -0.00011200000153621659, -0.3539389967918396, 0.7807120084762573, -0.00007500000356230885, 0.6248909831047058, 0.7807120084762573, -0.00007500000356230885, 0.6248909831047058, 0.7209920287132263, -0.29793301224708557, 0.6256250143051147, 0.21797800064086914, -0.0902160033583641, 0.9717749953269958, 0.23658299446105957, 0, 0.9716110229492188, 0.1595889925956726, -0.06596100330352783, 0.9849770069122314, 0.17308400571346283, 0, 0.9849069714546204, 0.3504979908466339, -0.1447400003671646, 0.9253119826316833, 0.37970298528671265, 0, 0.925108015537262, 0.48558899760246277, -0.20147399604320526, 0.8506529927253723, 0.5266720056533813, 0, 0.8500679731369019, 0.5518630146980286, -0.5517799854278564, 0.6252880096435547, 0.16663099825382233, -0.16663099825382233, 0.9718379974365234, 0.12190800160169601, -0.12190800160169601, 0.9850260019302368, 0.2676680088043213, -0.2676680088043213, 0.9255849719047546, 0.37131500244140625, -0.37131500244140625, 0.8510289788246155, 0.29798200726509094, -0.7210670113563538, 0.6255149841308594, 0.0902160033583641, -0.21797800064086914, 0.9717749953269958, 0.06596100330352783, -0.1595889925956726, 0.9849770069122314, 0.1447400003671646, -0.3504979908466339, 0.9253119826316833, 0.20147399604320526, -0.48558899760246277, 0.8506529927253723, -0.00007500000356230885, -0.7807120084762573, 0.6248900294303894, 0, -0.23658299446105957, 0.9716110229492188, 0, -0.17308400571346283, 0.9849069714546204, 0, -0.37970298528671265, 0.925108015537262, 0, -0.5266720056533813, 0.8500679731369019, -0.00007500000356230885, -0.7807120084762573, 0.6248900294303894, -0.29793301224708557, -0.7209920287132263, 0.6256250143051147, -0.0902160033583641, -0.21797800064086914, 0.9717749953269958, 0, -0.23658299446105957, 0.9716110229492188, -0.06596100330352783, -0.1595889925956726, 0.9849770069122314, 0, -0.17308400571346283, 0.9849069714546204, -0.1447400003671646, -0.3504979908466339, 0.9253119826316833, 0, -0.37970298528671265, 0.925108015537262, -0.20147399604320526, -0.48558899760246277, 0.8506529927253723, 0, -0.5266720056533813, 0.8500679731369019, -0.5517799854278564, -0.5518630146980286, 0.6252880096435547, -0.16663099825382233, -0.16663099825382233, 0.9718379974365234, -0.12190800160169601, -0.12190800160169601, 0.9850260019302368, -0.2676680088043213, -0.2676680088043213, 0.9255849719047546, -0.37131500244140625, -0.37131500244140625, 0.8510289788246155, -0.7210670113563538, -0.29798200726509094, 0.6255149841308594, -0.21797800064086914, -0.0902160033583641, 0.9717749953269958, -0.1595889925956726, -0.06596100330352783, 0.9849770069122314, -0.3504979908466339, -0.1447400003671646, 0.9253119826316833, -0.48558899760246277, -0.20147399604320526, 0.8506529927253723, -0.7807120084762573, 0.00007500000356230885, 0.6248900294303894, -0.23658299446105957, 0, 0.9716110229492188, -0.17308400571346283, 0, 0.9849069714546204, -0.37970298528671265, 0, 0.925108015537262, -0.5266720056533813, 0, 0.8500679731369019, -0.7807120084762573, 0.00007500000356230885, 0.6248900294303894, -0.7209920287132263, 0.29793301224708557, 0.6256250143051147, -0.21797800064086914, 0.0902160033583641, 0.9717749953269958, -0.23658299446105957, 0, 0.9716110229492188, -0.1595889925956726, 0.06596100330352783, 0.9849770069122314, -0.17308400571346283, 0, 0.9849069714546204, -0.3504979908466339, 0.1447400003671646, 0.9253119826316833, -0.37970298528671265, 0, 0.925108015537262, -0.48558899760246277, 0.20147399604320526, 0.8506529927253723, -0.5266720056533813, 0, 0.8500679731369019, -0.5518630146980286, 0.5517799854278564, 0.6252880096435547, -0.16663099825382233, 0.16663099825382233, 0.9718379974365234, -0.12190800160169601, 0.12190800160169601, 0.9850260019302368, -0.2676680088043213, 0.2676680088043213, 0.9255849719047546, -0.37131500244140625, 0.37131500244140625, 0.8510289788246155, -0.29798200726509094, 0.7210670113563538, 0.6255149841308594, -0.0902160033583641, 0.21797800064086914, 0.9717749953269958, -0.06596100330352783, 0.1595889925956726, 0.9849770069122314, -0.1447400003671646, 0.3504979908466339, 0.9253119826316833, -0.20147399604320526, 0.48558899760246277, 0.8506529927253723, 0.00007500000356230885, 0.7807120084762573, 0.6248900294303894, 0, 0.23658299446105957, 0.9716110229492188, 0, 0.17308400571346283, 0.9849069714546204, 0, 0.37970298528671265, 0.925108015537262, 0, 0.5266720056533813, 0.8500679731369019, 0.00007500000356230885, 0.7807120084762573, 0.6248900294303894, 0.29793301224708557, 0.7209920287132263, 0.6256250143051147, 0.0902160033583641, 0.21797800064086914, 0.9717749953269958, 0, 0.23658299446105957, 0.9716110229492188, 0.06596100330352783, 0.1595889925956726, 0.9849770069122314, 0, 0.17308400571346283, 0.9849069714546204, 0.1447400003671646, 0.3504979908466339, 0.9253119826316833, 0, 0.37970298528671265, 0.925108015537262, 0.20147399604320526, 0.48558899760246277, 0.8506529927253723, 0, 0.5266720056533813, 0.8500679731369019, 0.5517799854278564, 0.5518630146980286, 0.6252880096435547, 0.16663099825382233, 0.16663099825382233, 0.9718379974365234, 0.12190800160169601, 0.12190800160169601, 0.9850260019302368, 0.2676680088043213, 0.2676680088043213, 0.9255849719047546, 0.37131500244140625, 0.37131500244140625, 0.8510289788246155, 0.7210670113563538, 0.29798200726509094, 0.6255149841308594, 0.21797800064086914, 0.0902160033583641, 0.9717749953269958, 0.1595889925956726, 0.06596100330352783, 0.9849770069122314, 0.3504979908466339, 0.1447400003671646, 0.9253119826316833, 0.48558899760246277, 0.20147399604320526, 0.8506529927253723, 0.7807120084762573, -0.00007500000356230885, 0.6248909831047058, 0.23658299446105957, 0, 0.9716110229492188, 0.17308400571346283, 0, 0.9849069714546204, 0.37970298528671265, 0, 0.925108015537262, 0.5266720056533813, 0, 0.8500679731369019 ]); var teapotTangents = new Float32Array([ 0.012897999957203865, 0.998727023601532, -0.048757001757621765, 0.3861910104751587, 0.9210079908370972, -0.016421999782323837, 0.38136398792266846, 0.9230089783668518, 0.000155999994603917, 0.012866999953985214, 0.9987300038337708, 0.04870200157165527, 0.3750790059566498, 0.9061710238456726, -0.0007169999880716205, 0.19210100173950195, 0.9812139868736267, 0.01775900088250637, 0.3782620131969452, 0.9142940044403076, -0.00011300000187475234, 0.10451500117778778, 0.9897350072860718, -0.09747499972581863, 0.3655939996242523, 0.9257190227508545, 0.028463000431656837, 0.04767199978232384, 0.9953050017356873, -0.08423800021409988, 0.7092679738998413, 0.7031199932098389, -0.016364000737667084, 0.7061989903450012, 0.7061989903450012, 0, 0.6937360167503357, 0.6937360167503357, 0, 0.6997770071029663, 0.6997770071029663, 0, 0.6924030184745789, 0.7150859832763672, 0.02822900004684925, 0.9243540167808533, 0.37810400128364563, -0.01657800003886223, 0.9230089783668518, 0.38136398792266846, -0.000155999994603917, 0.9061710238456726, 0.3750790059566498, 0.0007169999880716205, 0.9142940044403076, 0.3782620131969452, 0.00011300000187475234, 0.9133660197257996, 0.39544400572776794, 0.028490999713540077, 0.9987040162086487, 0.015853000804781914, 0.04836999997496605, 0.9987369775772095, 0.014649000018835068, -0.04806999862194061, 0.9812150001525879, 0.19211700558662415, -0.01754000037908554, 0.9897350072860718, 0.10452800244092941, 0.09745799750089645, 0.9953050017356873, 0.04767199978232384, 0.08423800021409988, 0.9988179802894592, -0.009758999571204185, -0.047600001096725464, 0.9094679951667786, -0.4095839858055115, -0.012636999599635601, 0.9240090250968933, -0.3811509907245636, -0.0003150000120513141, 0.9987890124320984, -0.01066299993544817, 0.04801800101995468, 0.9072269797325134, -0.37142300605773926, 0.0207310002297163, 0.9814350008964539, -0.19095200300216675, 0.01795700006186962, 0.914870023727417, -0.3771440088748932, -0.0011480000102892518, 0.989749014377594, -0.10442499816417694, -0.09742700308561325, 0.925815999507904, -0.3653950095176697, 0.028308000415563583, 0.9953050017356873, -0.04767199978232384, -0.08423800021409988, 0.6768929958343506, -0.7314029932022095, -0.01988700032234192, 0.6994619965553284, -0.7145140171051025, -0.00029799999902024865, 0.6940590143203735, -0.6933979988098145, 0.015560000203549862, 0.7002580165863037, -0.6996300220489502, -0.000783999974373728, 0.715142011642456, -0.6923869848251343, 0.028078999370336533, 0.351936012506485, -0.933899998664856, -0.019843999296426773, 0.36654001474380493, -0.9298419952392578, -0.0005210000090301037, 0.37116900086402893, -0.9084830284118652, 0.00152299995534122, 0.3776479959487915, -0.9147650003433228, -0.00011000000085914508, 0.39533698558807373, -0.9134349822998047, 0.028410999104380608, 0.0013210000470280647, -0.9989479780197144, 0.045830998569726944, 0.003897000104188919, -0.9988909959793091, -0.04690299928188324, 0.18705999851226807, -0.9821630120277405, -0.018818000331521034, 0.10363999754190445, -0.9898579716682434, 0.09715499728918076, 0.04757700115442276, -0.9953129887580872, 0.08418799936771393, -0.02296699956059456, -0.9986780285835266, -0.04599199816584587, -0.3861910104751587, -0.9210079908370972, -0.016421999782323837, -0.38136398792266846, -0.9230089783668518, 0.000155999994603917, -0.020431000739336014, -0.9987260103225708, 0.04614400118589401, -0.3750790059566498, -0.9061710238456726, -0.0007169999880716205, -0.19216600060462952, -0.9812189936637878, 0.01677200011909008, -0.3782620131969452, -0.9142940044403076, -0.00011300000187475234, -0.10471200197935104, -0.9897390007972717, -0.09722500294446945, -0.3655939996242523, -0.9257190227508545, 0.028463000431656837, -0.047710999846458435, -0.9953050017356873, -0.08420699834823608, -0.7092679738998413, -0.7031199932098389, -0.016364000737667084, -0.7061989903450012, -0.7061989903450012, 0, -0.6937360167503357, -0.6937360167503357, 0, -0.6997770071029663, -0.6997770071029663, 0, -0.6924030184745789, -0.7150859832763672, 0.02822900004684925, -0.9243540167808533, -0.37810400128364563, -0.01657800003886223, -0.9230089783668518, -0.38136398792266846, -0.000155999994603917, -0.9061710238456726, -0.3750790059566498, 0.0007169999880716205, -0.9142940044403076, -0.3782620131969452, 0.00011300000187475234, -0.9133660197257996, -0.39544400572776794, 0.028490999713540077, -0.998727023601532, -0.012897999957203865, 0.048757001757621765, -0.9987300038337708, -0.012866999953985214, -0.04870200157165527, -0.9812139868736267, -0.19210100173950195, -0.01775900088250637, -0.9897350072860718, -0.10451500117778778, 0.09747499972581863, -0.9953050017356873, -0.04767199978232384, 0.08423800021409988, -0.998727023601532, 0.012897999957203865, -0.048757001757621765, -0.9210079908370972, 0.3861910104751587, -0.016421999782323837, -0.9230089783668518, 0.38136398792266846, 0.000155999994603917, -0.9987300038337708, 0.012866999953985214, 0.04870200157165527, -0.9061710238456726, 0.3750790059566498, -0.0007169999880716205, -0.9812139868736267, 0.19210100173950195, 0.01775900088250637, -0.9142940044403076, 0.3782620131969452, -0.00011300000187475234, -0.9897350072860718, 0.10451500117778778, -0.09747499972581863, -0.9257190227508545, 0.3655939996242523, 0.028463000431656837, -0.9953050017356873, 0.04767199978232384, -0.08423800021409988, -0.7031199932098389, 0.7092679738998413, -0.016364000737667084, -0.7061989903450012, 0.7061989903450012, 0, -0.6937360167503357, 0.6937360167503357, 0, -0.6997770071029663, 0.6997770071029663, 0, -0.7150859832763672, 0.6924030184745789, 0.02822900004684925, -0.37810400128364563, 0.9243540167808533, -0.01657800003886223, -0.38136398792266846, 0.9230089783668518, -0.000155999994603917, -0.3750790059566498, 0.9061710238456726, 0.0007169999880716205, -0.3782620131969452, 0.9142940044403076, 0.00011300000187475234, -0.39544400572776794, 0.9133660197257996, 0.028490999713540077, -0.012897999957203865, 0.998727023601532, 0.048757001757621765, -0.012866999953985214, 0.9987300038337708, -0.04870200157165527, -0.19210100173950195, 0.9812139868736267, -0.01775900088250637, -0.10451500117778778, 0.9897350072860718, 0.09747499972581863, -0.04767199978232384, 0.9953050017356873, 0.08423800021409988, 0.04767199978232384, 0.9953050017356873, -0.08423800021409988, 0.39544400572776794, 0.9133660197257996, -0.028490999713540077, 0.38111698627471924, 0.9210190176963806, -0.000015999999959603883, 0.031922999769449234, 0.9968529939651489, -0.07255599647760391, 0.3815299868583679, 0.9219080209732056, 0.0000019999999949504854, 0.022261999547481537, 0.9978039860725403, -0.06237399950623512, 0.3821389973163605, 0.9231889843940735, 0.00001700000029813964, 0.008317999541759491, 0.9991790056228638, -0.03964800015091896, 0.38228899240493774, 0.9239469766616821, -0.004430000204592943, 0.0008660000166855752, 0.9999139904975891, 0.013048999942839146, 0.7150859832763672, 0.6924030184745789, -0.02822900004684925, 0.7048519849777222, 0.7048519849777222, 0, 0.7055330276489258, 0.7055330276489258, 0, 0.7065179944038391, 0.7065179944038391, 0, 0.7068390250205994, 0.707252025604248, -0.004379999823868275, 0.9257190227508545, 0.3655939996242523, -0.028463000431656837, 0.9210180044174194, 0.38111698627471924, 0.000015999999959603883, 0.9219080209732056, 0.3815299868583679, -0.0000019999999949504854, 0.9231889843940735, 0.3821389973163605, -0.00001700000029813964, 0.9237229824066162, 0.38283199071884155, -0.004399999976158142, 0.9953050017356873, 0.04767199978232384, 0.08423800021409988, 0.9968529939651489, 0.031922999769449234, 0.07255599647760391, 0.9978039860725403, 0.022261999547481537, 0.06237399950623512, 0.9991790056228638, 0.008317999541759491, 0.03964800015091896, 0.9999139904975891, 0.0008660000166855752, -0.013048999942839146, 0.9953050017356873, -0.04767199978232384, -0.08423800021409988, 0.9135000109672546, -0.3951619863510132, -0.02861100062727928, 0.9210190176963806, -0.38111698627471924, -0.000015999999959603883, 0.9968529939651489, -0.031922999769449234, -0.07255599647760391, 0.9219080209732056, -0.3815299868583679, 0.0000019999999949504854, 0.9978039860725403, -0.022261999547481537, -0.06237399950623512, 0.9231889843940735, -0.3821389973163605, 0.00001700000029813964, 0.9991790056228638, -0.008317999541759491, -0.03964800015091896, 0.9239469766616821, -0.38228899240493774, -0.004430000204592943, 0.9999139904975891, -0.0008660000166855752, 0.013048999942839146, 0.6925899982452393, -0.7149369716644287, -0.028262000530958176, 0.7048519849777222, -0.7048519849777222, 0, 0.7055330276489258, -0.7055330276489258, 0, 0.7065179944038391, -0.7065179944038391, 0, 0.707252025604248, -0.7068390250205994, -0.004379999823868275, 0.3656100034713745, -0.9257280230522156, -0.02841299958527088, 0.38111698627471924, -0.9210180044174194, 0.000015999999959603883, 0.3815299868583679, -0.9219080209732056, -0.0000019999999949504854, 0.3821389973163605, -0.9231889843940735, -0.00001700000029813964, 0.38283199071884155, -0.9237229824066162, -0.004399999976158142, 0.04757700115442276, -0.9953129887580872, 0.08418799936771393, 0.031922999769449234, -0.9968529939651489, 0.07255599647760391, 0.022261999547481537, -0.9978039860725403, 0.06237399950623512, 0.008317999541759491, -0.9991790056228638, 0.03964800015091896, 0.0008660000166855752, -0.9999139904975891, -0.013048999942839146, -0.047710999846458435, -0.9953050017356873, -0.08420699834823608, -0.39544400572776794, -0.9133660197257996, -0.028490999713540077, -0.38111698627471924, -0.9210190176963806, -0.000015999999959603883, -0.031922999769449234, -0.9968529939651489, -0.07255599647760391, -0.3815299868583679, -0.9219080209732056, 0.0000019999999949504854, -0.022261999547481537, -0.9978039860725403, -0.06237399950623512, -0.3821389973163605, -0.9231889843940735, 0.00001700000029813964, -0.008317999541759491, -0.9991790056228638, -0.03964800015091896, -0.38228899240493774, -0.9239469766616821, -0.004430000204592943, -0.0008660000166855752, -0.9999139904975891, 0.013048999942839146, -0.7150859832763672, -0.6924030184745789, -0.02822900004684925, -0.7048519849777222, -0.7048519849777222, 0, -0.7055330276489258, -0.7055330276489258, 0, -0.7065179944038391, -0.7065179944038391, 0, -0.7068390250205994, -0.707252025604248, -0.004379999823868275, -0.9257190227508545, -0.3655939996242523, -0.028463000431656837, -0.9210180044174194, -0.38111698627471924, 0.000015999999959603883, -0.9219080209732056, -0.3815299868583679, -0.0000019999999949504854, -0.9231889843940735, -0.3821389973163605, -0.00001700000029813964, -0.9237229824066162, -0.38283199071884155, -0.004399999976158142, -0.9953050017356873, -0.04767199978232384, 0.08423800021409988, -0.9968529939651489, -0.031922999769449234, 0.07255599647760391, -0.9978039860725403, -0.022261999547481537, 0.06237399950623512, -0.9991790056228638, -0.008317999541759491, 0.03964800015091896, -0.9999139904975891, -0.0008660000166855752, -0.013048999942839146, -0.9953050017356873, 0.04767199978232384, -0.08423800021409988, -0.9133660197257996, 0.39544400572776794, -0.028490999713540077, -0.9210190176963806, 0.38111698627471924, -0.000015999999959603883, -0.9968529939651489, 0.031922999769449234, -0.07255599647760391, -0.9219080209732056, 0.3815299868583679, 0.0000019999999949504854, -0.9978039860725403, 0.022261999547481537, -0.06237399950623512, -0.9231889843940735, 0.3821389973163605, 0.00001700000029813964, -0.9991790056228638, 0.008317999541759491, -0.03964800015091896, -0.9239469766616821, 0.38228899240493774, -0.004430000204592943, -0.9999139904975891, 0.0008660000166855752, 0.013048999942839146, -0.6924030184745789, 0.7150859832763672, -0.02822900004684925, -0.7048519849777222, 0.7048519849777222, 0, -0.7055330276489258, 0.7055330276489258, 0, -0.7065179944038391, 0.7065179944038391, 0, -0.707252025604248, 0.7068390250205994, -0.004379999823868275, -0.3655939996242523, 0.9257190227508545, -0.028463000431656837, -0.38111698627471924, 0.9210180044174194, 0.000015999999959603883, -0.3815299868583679, 0.9219080209732056, -0.0000019999999949504854, -0.3821389973163605, 0.9231889843940735, -0.00001700000029813964, -0.38283199071884155, 0.9237229824066162, -0.004399999976158142, -0.04767199978232384, 0.9953050017356873, 0.08423800021409988, -0.031922999769449234, 0.9968529939651489, 0.07255599647760391, -0.022261999547481537, 0.9978039860725403, 0.06237399950623512, -0.008317999541759491, 0.9991790056228638, 0.03964800015091896, -0.0008660000166855752, 0.9999139904975891, -0.013048999942839146, 0.0008660000166855752, 0.9999139904975891, 0.013048999942839146, 0.38283199071884155, 0.9237229824066162, 0.004399999976158142, 0.38101500272750854, 0.9204739928245544, -0.00003899999865097925, 0.03731299936771393, 0.9963229894638062, 0.07712399959564209, 0.37877199053764343, 0.9154880046844482, 0.00008399999933317304, 0.09151100367307663, 0.9910060167312622, 0.097632996737957, 0.378387987613678, 0.9145749807357788, 0.00009999999747378752, 0.10134600102901459, 0.9900450110435486, 0.09767600148916245, 0.356795996427536, 0.9266510009765625, -0.03188199922442436, 0.07246600091457367, 0.9928709864616394, 0.09463199973106384, 0.707252025604248, 0.7068390250205994, 0.004379999823868275, 0.7044739723205566, 0.7044739723205566, 0, 0.7006790041923523, 0.7006790041923523, 0, 0.6999930143356323, 0.6999930143356323, 0, 0.6847820281982422, 0.7192310094833374, -0.03167999908328056, 0.9239469766616821, 0.38228899240493774, 0.004430000204592943, 0.9204739928245544, 0.38101500272750854, 0.00003899999865097925, 0.9154880046844482, 0.37877199053764343, -0.00008399999933317304, 0.9145749807357788, 0.378387987613678, -0.00009999999747378752, 0.9078760147094727, 0.40216198563575745, -0.03206299990415573, 0.9999139904975891, 0.0008660000166855752, -0.013048999942839146, 0.9963229894638062, 0.03731299936771393, -0.07712399959564209, 0.9910060167312622, 0.09151100367307663, -0.097632996737957, 0.9900450110435486, 0.10134600102901459, -0.09767600148916245, 0.9928709864616394, 0.07246600091457367, -0.09463199973106384, 0.9999139904975891, -0.0008660000166855752, 0.013048999942839146, 0.9237229824066162, -0.38283199071884155, 0.004399999976158142, 0.9204739928245544, -0.38101500272750854, -0.00003899999865097925, 0.9963229894638062, -0.03731299936771393, 0.07712399959564209, 0.9154880046844482, -0.37877199053764343, 0.00008399999933317304, 0.9910060167312622, -0.09151100367307663, 0.097632996737957, 0.9145749807357788, -0.378387987613678, 0.00009999999747378752, 0.9900450110435486, -0.10134600102901459, 0.09767600148916245, 0.9266510009765625, -0.356795996427536, -0.03188199922442436, 0.9928709864616394, -0.07246600091457367, 0.09463199973106384, 0.7068390250205994, -0.707252025604248, 0.004379999823868275, 0.7044739723205566, -0.7044739723205566, 0, 0.7006790041923523, -0.7006790041923523, 0, 0.6999930143356323, -0.6999930143356323, 0, 0.7192310094833374, -0.6847820281982422, -0.03167999908328056, 0.38228899240493774, -0.9239469766616821, 0.004430000204592943, 0.38101500272750854, -0.9204739928245544, 0.00003899999865097925, 0.37877199053764343, -0.9154880046844482, -0.00008399999933317304, 0.378387987613678, -0.9145749807357788, -0.00009999999747378752, 0.40216198563575745, -0.9078760147094727, -0.03206299990415573, 0.0008660000166855752, -0.9999139904975891, -0.013048999942839146, 0.03731299936771393, -0.9963229894638062, -0.07712399959564209, 0.09151100367307663, -0.9910060167312622, -0.097632996737957, 0.10134600102901459, -0.9900450110435486, -0.09767600148916245, 0.07246600091457367, -0.9928709864616394, -0.09463199973106384, -0.0008660000166855752, -0.9999139904975891, 0.013048999942839146, -0.38283199071884155, -0.9237229824066162, 0.004399999976158142, -0.38101500272750854, -0.9204739928245544, -0.00003899999865097925, -0.03731299936771393, -0.9963229894638062, 0.07712399959564209, -0.37877199053764343, -0.9154880046844482, 0.00008399999933317304, -0.09151100367307663, -0.9910060167312622, 0.097632996737957, -0.378387987613678, -0.9145749807357788, 0.00009999999747378752, -0.10134600102901459, -0.9900450110435486, 0.09767600148916245, -0.356795996427536, -0.9266510009765625, -0.03188199922442436, -0.07246600091457367, -0.9928709864616394, 0.09463199973106384, -0.707252025604248, -0.7068390250205994, 0.004379999823868275, -0.7044739723205566, -0.7044739723205566, 0, -0.7006790041923523, -0.7006790041923523, 0, -0.6999930143356323, -0.6999930143356323, 0, -0.6847820281982422, -0.7192310094833374, -0.03167999908328056, -0.9239469766616821, -0.38228899240493774, 0.004430000204592943, -0.9204739928245544, -0.38101500272750854, 0.00003899999865097925, -0.9154880046844482, -0.37877199053764343, -0.00008399999933317304, -0.9145749807357788, -0.378387987613678, -0.00009999999747378752, -0.9078760147094727, -0.40216198563575745, -0.03206299990415573, -0.9999139904975891, -0.0008660000166855752, -0.013048999942839146, -0.9963229894638062, -0.03731299936771393, -0.07712399959564209, -0.9910060167312622, -0.09151100367307663, -0.097632996737957, -0.9900450110435486, -0.10134600102901459, -0.09767600148916245, -0.9928709864616394, -0.07246600091457367, -0.09463199973106384, -0.9999139904975891, 0.0008660000166855752, 0.013048999942839146, -0.9237229824066162, 0.38283199071884155, 0.004399999976158142, -0.9204739928245544, 0.38101500272750854, -0.00003899999865097925, -0.9963229894638062, 0.03731299936771393, 0.07712399959564209, -0.9154880046844482, 0.37877199053764343, 0.00008399999933317304, -0.9910060167312622, 0.09151100367307663, 0.097632996737957, -0.9145749807357788, 0.378387987613678, 0.00009999999747378752, -0.9900450110435486, 0.10134600102901459, 0.09767600148916245, -0.9266510009765625, 0.356795996427536, -0.03188199922442436, -0.9928709864616394, 0.07246600091457367, 0.09463199973106384, -0.7068390250205994, 0.707252025604248, 0.004379999823868275, -0.7044739723205566, 0.7044739723205566, 0, -0.7006790041923523, 0.7006790041923523, 0, -0.6999930143356323, 0.6999930143356323, 0, -0.7192310094833374, 0.6847820281982422, -0.03167999908328056, -0.38228899240493774, 0.9239469766616821, 0.004430000204592943, -0.38101500272750854, 0.9204739928245544, 0.00003899999865097925, -0.37877199053764343, 0.9154880046844482, -0.00008399999933317304, -0.378387987613678, 0.9145749807357788, -0.00009999999747378752, -0.40216198563575745, 0.9078760147094727, -0.03206299990415573, -0.0008660000166855752, 0.9999139904975891, -0.013048999942839146, -0.03731299936771393, 0.9963229894638062, -0.07712399959564209, -0.09151100367307663, 0.9910060167312622, -0.097632996737957, -0.10134600102901459, 0.9900450110435486, -0.09767600148916245, -0.07246600091457367, 0.9928709864616394, -0.09463199973106384, 0.07246600091457367, 0.9928709864616394, 0.09463199973106384, 0.40216198563575745, 0.9078760147094727, 0.03206299990415573, 0.37766799330711365, 0.912958025932312, 0.00018099999579135329, 0.11919300258159637, 0.9883019924163818, 0.09514500200748444, 0.37516000866889954, 0.906607985496521, 0.00016799999866634607, 0.187733992934227, 0.9816380143165588, 0.03381900116801262, 0.2823430001735687, 0.767549991607666, -0.1682250052690506, 0.12883399426937103, 0.6540690064430237, -0.32698601484298706, 0.06457000225782394, 0.32701900601387024, -0.6666669845581055, 0, 0, -1, 0.7192320227622986, 0.6847820281982422, 0.03167999908328056, 0.6987630128860474, 0.6987630128860474, 0, 0.694034993648529, 0.694034993648529, 0, 0.5551990270614624, 0.6008960008621216, -0.16825300455093384, 0.1854030042886734, 0.27701398730278015, -0.6666669845581055, 0.9266499876976013, 0.3567950129508972, 0.03188199922442436, 0.912958025932312, 0.37766799330711365, -0.00018099999579135329, 0.906607985496521, 0.37516000866889954, -0.00016799999866634607, 0.742605984210968, 0.3426159918308258, -0.1683180034160614, 0.27701398730278015, 0.1854030042886734, -0.6666669845581055, 0.9928709864616394, 0.07246600091457367, -0.09463199973106384, 0.9883019924163818, 0.11919300258159637, -0.09514500200748444, 0.9816370010375977, 0.187733992934227, -0.03381900116801262, 0.9811030030250549, 0.19325199723243713, -0.009519999846816063, 0.49052900075912476, 0.0968559980392456, -0.5, 0.9928709864616394, -0.07246600091457367, 0.09463199973106384, 0.9078760147094727, -0.40216198563575745, 0.03206299990415573, 0.912958025932312, -0.37766799330711365, 0.00018099999579135329, 0.9883019924163818, -0.11919300258159637, 0.09514500200748444, 0.906607985496521, -0.37516000866889954, 0.00016799999866634607, 0.9816380143165588, -0.187733992934227, 0.03381900116801262, 0.767549991607666, -0.2823430001735687, -0.1682250052690506, 0.6540690064430237, -0.12883399426937103, -0.32698601484298706, 0.32701900601387024, -0.06457000225782394, -0.6666669845581055, 0, 0, -1, 0.6847820281982422, -0.7192320227622986, 0.03167999908328056, 0.6987630128860474, -0.6987630128860474, 0, 0.694034993648529, -0.694034993648529, 0, 0.6008960008621216, -0.5551990270614624, -0.16825300455093384, 0.27701398730278015, -0.1854030042886734, -0.6666669845581055, 0.3567950129508972, -0.9266499876976013, 0.03188199922442436, 0.37766799330711365, -0.912958025932312, -0.00018099999579135329, 0.37516000866889954, -0.906607985496521, -0.00016799999866634607, 0.3426159918308258, -0.742605984210968, -0.1683180034160614, 0.1854030042886734, -0.27701398730278015, -0.6666669845581055, 0.07246600091457367, -0.9928709864616394, -0.09463199973106384, 0.11919300258159637, -0.9883019924163818, -0.09514500200748444, 0.187733992934227, -0.9816370010375977, -0.03381900116801262, 0.19325199723243713, -0.9811030030250549, -0.009519999846816063, 0.0968559980392456, -0.49052900075912476, -0.5, -0.07246600091457367, -0.9928709864616394, 0.09463199973106384, -0.40216198563575745, -0.9078760147094727, 0.03206299990415573, -0.37766799330711365, -0.912958025932312, 0.00018099999579135329, -0.11919300258159637, -0.9883019924163818, 0.09514500200748444, -0.37516000866889954, -0.906607985496521, 0.00016799999866634607, -0.187733992934227, -0.9816380143165588, 0.03381900116801262, -0.2823430001735687, -0.767549991607666, -0.1682250052690506, -0.12883399426937103, -0.6540690064430237, -0.32698601484298706, -0.06457000225782394, -0.32701900601387024, -0.6666669845581055, 0, 0, -1, -0.7192320227622986, -0.6847820281982422, 0.03167999908328056, -0.6987630128860474, -0.6987630128860474, 0, -0.694034993648529, -0.694034993648529, 0, -0.5551990270614624, -0.6008960008621216, -0.16825300455093384, -0.1854030042886734, -0.27701398730278015, -0.6666669845581055, -0.9266499876976013, -0.3567950129508972, 0.03188199922442436, -0.912958025932312, -0.37766799330711365, -0.00018099999579135329, -0.906607985496521, -0.37516000866889954, -0.00016799999866634607, -0.742605984210968, -0.3426159918308258, -0.1683180034160614, -0.27701398730278015, -0.1854030042886734, -0.6666669845581055, -0.9928709864616394, -0.07246600091457367, -0.09463199973106384, -0.9883019924163818, -0.11919300258159637, -0.09514500200748444, -0.9816370010375977, -0.187733992934227, -0.03381900116801262, -0.9811030030250549, -0.19325199723243713, -0.009519999846816063, -0.49052900075912476, -0.0968559980392456, -0.5, -0.9928709864616394, 0.07246600091457367, 0.09463199973106384, -0.9078760147094727, 0.40216198563575745, 0.03206299990415573, -0.912958025932312, 0.37766799330711365, 0.00018099999579135329, -0.9883019924163818, 0.11919300258159637, 0.09514500200748444, -0.906607985496521, 0.37516000866889954, 0.00016799999866634607, -0.9816380143165588, 0.187733992934227, 0.03381900116801262, -0.767549991607666, 0.2823430001735687, -0.1682250052690506, -0.6540690064430237, 0.12883399426937103, -0.32698601484298706, -0.32701900601387024, 0.06457000225782394, -0.6666669845581055, 0, 0, -1, -0.6847820281982422, 0.7192320227622986, 0.03167999908328056, -0.6987630128860474, 0.6987630128860474, 0, -0.694034993648529, 0.694034993648529, 0, -0.6008960008621216, 0.5551990270614624, -0.16825300455093384, -0.27701398730278015, 0.1854030042886734, -0.6666669845581055, -0.3567950129508972, 0.9266499876976013, 0.03188199922442436, -0.37766799330711365, 0.912958025932312, -0.00018099999579135329, -0.37516000866889954, 0.906607985496521, -0.00016799999866634607, -0.3426159918308258, 0.742605984210968, -0.1683180034160614, -0.1854030042886734, 0.27701398730278015, -0.6666669845581055, -0.07246600091457367, 0.9928709864616394, -0.09463199973106384, -0.11919300258159637, 0.9883019924163818, -0.09514500200748444, -0.187733992934227, 0.9816370010375977, -0.03381900116801262, -0.19325199723243713, 0.9811030030250549, -0.009519999846816063, -0.0968559980392456, 0.49052900075912476, -0.5, -0.006597999949008226, 0.9961680173873901, 0.0001630000042496249, -0.043907999992370605, 0.779125988483429, -0.55936598777771, 0.23287899792194366, 0.79271000623703, -0.506534993648529, 0.11139900237321854, 0.9923329949378967, 0.0053449999541044235, 0.4521920084953308, 0.7370989918708801, -0.42180201411247253, 0.17797799408435822, 0.9827970266342163, 0.036841001361608505, 0.6075379848480225, 0.7066869735717773, -0.270797997713089, 0.11894699931144714, 0.9864829778671265, 0.10517799854278564, 0.6583719849586487, 0.7438470125198364, -0.06727500259876251, 0.0010629999451339245, 0.99891597032547, 0.04653400182723999, -0.1622990071773529, -0.14869500696659088, -0.9069569706916809, 0.3020159900188446, -0.014301000162959099, -0.8847119808197021, 0.7048640251159668, -0.042514998465776443, -0.6788020133972168, 0.8948519825935364, -0.11078000068664551, -0.38824599981307983, 0.9622920155525208, -0.09367900341749191, -0.14349600672721863, -0.12511900067329407, -0.8479049801826477, -0.4783349931240082, 0.11315400153398514, -0.8153669834136963, -0.5167160034179688, 0.3956319987773895, -0.7910019755363464, -0.4345270097255707, 0.5244609713554382, -0.8012329936027527, -0.2643829882144928, 0.571465015411377, -0.7902160286903381, -0.12332800030708313, -0.0943560004234314, -0.9955379962921143, -0.0010989999864250422, 0.012040999718010426, -0.9965500235557556, 0, 0.09501499682664871, -0.9936969876289368, 0.02440500073134899, 0.03737499937415123, -0.9978089928627014, 0.035909999161958694, -0.0008800000068731606, -0.9973530173301697, -0.04031199961900711, 0.007164000067859888, -0.9961649775505066, -0.00002700000004551839, 0.043988000601530075, -0.8330309987068176, 0.4691329896450043, -0.2334270030260086, -0.7983189821243286, 0.49840399622917175, -0.10737399756908417, -0.9927549958229065, -0.007029999978840351, -0.45147499442100525, -0.7576299905776978, 0.39375001192092896, -0.15364399552345276, -0.9863160252571106, -0.048294998705387115, -0.5575600266456604, -0.7753210067749023, 0.2001740038394928, -0.07242999970912933, -0.9923030138015747, -0.08845999836921692, -0.5877019762992859, -0.8041930198669434, 0.04768599942326546, 0.0005830000154674053, -0.9997940063476562, -0.020301999524235725, 0.13663700222969055, -0.14665700495243073, 0.8966140151023865, -0.3045389950275421, -0.012237999588251114, 0.8833180069923401, -0.7020289897918701, -0.033987998962402344, 0.6724730134010315, -0.8890330195426941, -0.09636799991130829, 0.37605398893356323, -0.9668099880218506, -0.08601800352334976, 0.1358419954776764, 0.12022499740123749, 0.7918559908866882, 0.5693140029907227, -0.11313500255346298, 0.8111780285835266, 0.5236610174179077, -0.39790698885917664, 0.7734419703483582, 0.45853298902511597, -0.5793390274047852, 0.7346490025520325, 0.32973799109458923, -0.6447499990463257, 0.7340419888496399, 0.12459299713373184, 0.09378799796104431, 0.9955919981002808, 0.000944000028539449, -0.01607999950647354, 0.9964879751205444, 0.00035600000410340726, -0.11933200061321259, 0.9912199974060059, -0.01737299934029579, -0.08618299663066864, 0.9940080046653748, -0.053598999977111816, -0.004110999871045351, 0.9980229735374451, 0.015703000128269196, 0.010142000392079353, 0.9933879971504211, 0.10034400224685669, 0.6597890257835388, 0.7114480137825012, 0.12964099645614624, 0.5634239912033081, 0.7594000101089478, 0.289902001619339, -0.021227000281214714, 0.9976930022239685, 0.05189099907875061, 0.3972559869289398, 0.7709670066833496, 0.45872700214385986, -0.05054600164294243, 0.9957669973373413, 0.060869000852108, 0.11805199831724167, 0.7611619830131531, 0.5692800283432007, -0.11414600163698196, 0.9869359731674194, 0.08862999826669693, -0.0012870000209659338, 0.7195389866828918, 0.6293820142745972, -0.18971200287342072, 0.9752820134162903, 0.11328700184822083, 0.9685969948768616, -0.08966200053691864, 0.13331100344657898, 0.8902140259742737, -0.051961999386548996, 0.39323100447654724, 0.6728280186653137, -0.050324998795986176, 0.6965069770812988, 0.25133201479911804, -0.04306900128722191, 0.9169719815254211, -0.19813700020313263, -0.2512879967689514, 0.9046909809112549, 0.5937719941139221, -0.8024669885635376, 0.03307799994945526, 0.5571249723434448, -0.7907459735870361, 0.2022089958190918, 0.4313510060310364, -0.8083119988441467, 0.37996000051498413, 0.19395600259304047, -0.8197799921035767, 0.5133119821548462, -0.1517219990491867, -0.8084930181503296, 0.5055829882621765, 0.0035200000274926424, -0.9997940063476562, 0.019979000091552734, 0.01159599982202053, -0.9981369972229004, -0.02326199971139431, 0.01310999970883131, -0.9988970160484314, -0.008480999618768692, -0.02485400065779686, -0.9978809952735901, 0.021263999864459038, -0.11335399746894836, -0.9881970286369324, 0.06441199779510498, -0.0035459999926388264, -0.9954169988632202, -0.07682599872350693, -0.5816869735717773, -0.7760900259017944, -0.13957500457763672, -0.5260769724845886, -0.790789008140564, -0.2781960070133209, 0.017288999632000923, -0.9983699917793274, -0.03728000074625015, -0.36800798773765564, -0.7982890009880066, -0.4405499994754791, 0.03743100166320801, -0.9973520040512085, -0.03640099987387657, -0.09636899828910828, -0.7829139828681946, -0.5500450134277344, 0.10426300019025803, -0.9894949793815613, -0.06746900081634521, 0.10083399713039398, -0.8161320090293884, -0.48112401366233826, 0.18510299921035767, -0.9776470065116882, -0.09971100091934204, -0.9615049958229065, -0.08203399926424026, -0.14958199858665466, -0.8876789808273315, -0.04622500017285347, -0.39955899119377136, -0.6675580143928528, -0.03723999857902527, -0.7007560133934021, -0.245511993765831, -0.03216199949383736, -0.9151920080184937, 0.15477199852466583, -0.24929499626159668, -0.8975690007209778, -0.6700729727745056, 0.7402250170707703, -0.01942499913275242, -0.5923460125923157, 0.7624830007553101, -0.21566900610923767, -0.45611900091171265, 0.7868310213088989, -0.39906400442123413, -0.21001900732517242, 0.8031420111656189, -0.5333020091056824, 0.05119999870657921, 0.7096909880638123, -0.6591699719429016, -0.014175999909639359, 0.9989240169525146, -0.04416000097990036, -0.0065449997782707214, 0.9983869791030884, 0.008813999593257904, 0.0023960000835359097, 0.9989259839057922, -0.016711000353097916, 0.03813000023365021, 0.9969249963760376, -0.04171599820256233, 0.11744900047779083, 0.986670970916748, -0.0799890011548996, -0.02072799950838089, -0.997963011264801, 0.0017740000039339066, 0.10236400365829468, -0.695684015750885, -0.6961740255355835, 0.28174999356269836, -0.7065439820289612, -0.6379269957542419, -0.027713999152183533, -0.9983959794044495, -0.016395000740885735, 0.4621469974517822, -0.7501789927482605, -0.43765199184417725, -0.014942999929189682, -0.9960020184516907, -0.04751100018620491, 0.6121799945831299, -0.7355859875679016, -0.1658719927072525, 0.08200599998235703, -0.9833409786224365, 0.11102399975061417, 0.7232419848442078, -0.6012910008430481, -0.14595800638198853, 0.32238098978996277, -0.9036369919776917, 0.28197699785232544, 0.1188960000872612, 0.09661199897527695, -0.9692260026931763, 0.3230240046977997, 0.06791900098323822, -0.9069269895553589, 0.6287810206413269, 0.00962899997830391, -0.711097002029419, 0.8952469825744629, -0.060169998556375504, -0.3366979956626892, 0.9689210057258606, -0.04508800059556961, -0.13095800578594208, 0.06500200182199478, 0.7708680033683777, -0.6083509922027588, 0.1816529929637909, 0.7457069754600525, -0.593995988368988, 0.37600401043891907, 0.7467949986457825, -0.4776870012283325, 0.6288849711418152, 0.7020969986915588, -0.27160701155662537, 0.8230010271072388, 0.5295370221138, -0.09450399875640869, -0.12820099294185638, 0.9899809956550598, -0.05917999893426895, -0.11097600311040878, 0.9872509837150574, -0.09937400370836258, -0.06767299771308899, 0.9865689873695374, -0.1427209973335266, -0.0003349999897181988, 0.9967420101165771, 0.025443999096751213, 0.29019099473953247, 0.9243509769439697, 0.1957239955663681, 0.07294999808073044, 0.9949049949645996, 0.03147900104522705, -0.04948300123214722, 0.7695090174674988, 0.6163870096206665, -0.24193400144577026, 0.7750219702720642, 0.5679330229759216, 0.05620399862527847, 0.9959489703178406, 0.052143000066280365, -0.4294399917125702, 0.779321014881134, 0.41615501046180725, 0.023887999355793, 0.9943940043449402, 0.07553800195455551, -0.6655910015106201, 0.6939520239830017, 0.20106400549411774, -0.09678799659013748, 0.9791589975357056, -0.12869000434875488, -0.7716730237007141, 0.5443729758262634, 0.1793539971113205, -0.417836993932724, 0.8721759915351868, -0.2544029951095581, -0.09499499946832657, 0.08934500068426132, 0.9787889719009399, -0.3299880027770996, 0.06701900064945221, 0.9273520112037659, -0.6511250138282776, 0.023523999378085136, 0.7280719876289368, -0.9116759896278381, -0.033263999968767166, 0.34162598848342896, -0.9896330237388611, -0.013496000319719315, 0.07834099978208542, -0.07044100016355515, -0.6954740285873413, 0.7080140113830566, -0.21969600021839142, -0.6959800124168396, 0.6642320156097412, -0.4075010120868683, -0.7370589971542358, 0.5047789812088013, -0.5866039991378784, -0.7473030090332031, 0.24636299908161163, -0.799036979675293, -0.5617390275001526, 0.05794600024819374, 0.07605399936437607, -0.9967970252037048, 0.02472200058400631, 0.08756300061941147, -0.9926980137825012, 0.05929899960756302, 0.07250799983739853, -0.9901790022850037, 0.11122000217437744, 0.015556000173091888, -0.9970260262489319, -0.011235999874770641, -0.194814994931221, -0.9439409971237183, -0.22127500176429749, 0.3417310118675232, -0.8896859884262085, 0.3012309968471527, 0.8375009894371033, -0.4931910037994385, 0.05739299952983856, 0.8273029923439026, -0.4684619903564453, -0.05539099872112274, 0.5311300158500671, -0.8121910095214844, 0.24026300013065338, 0.8069959878921509, -0.47689300775527954, 0.002638000063598156, 0.644743025302887, -0.7642210125923157, -0.015455000102519989, 0.8856800198554993, -0.4464530050754547, 0.047488000243902206, -0.011536000296473503, -0.999845027923584, -0.0008730000117793679, 0.7597830295562744, -0.6229599714279175, 0.026636000722646713, 0.321245014667511, -0.8855000138282776, 0.3356960117816925, 0.998091995716095, -0.005673000123351812, 0.025262000039219856, 0.9941530227661133, 0.046904999762773514, -0.00951599981635809, 0.9838590025901794, -0.00041700000292621553, 0.010572000406682491, 0.990556001663208, 0.01886500045657158, 0.04422200098633766, 0.9921990036964417, -0.12290599942207336, 0.011202000081539154, 0.828000009059906, 0.5258169770240784, -0.0846100002527237, 0.8704839944839478, 0.4878079891204834, 0.00635599996894598, 0.7773939967155457, 0.5659670233726501, -0.09634699672460556, 0.8190580010414124, 0.4740380048751831, 0.01190400030463934, 0.9017590284347534, 0.3486430048942566, -0.05601400136947632, 0.41038599610328674, 0.870602011680603, 0.27135801315307617, 0.3019320070743561, 0.8897680044174194, 0.34101900458335876, 0.13912299275398254, 0.9423390030860901, -0.3042120039463043, 0.6167309880256653, 0.7692840099334717, 0.1667650043964386, 0.5558350086212158, 0.8010749816894531, 0.21867799758911133, -0.4410029947757721, 0.8555399775505066, -0.2693159878253937, -0.8639690279960632, 0.464356005191803, -0.019222000613808632, -0.8705710172653198, 0.4855479896068573, -0.005623999983072281, -0.33969300985336304, 0.8762779831886292, -0.34097298979759216, -0.7608209848403931, 0.5840269923210144, 0.11236599832773209, -0.16763299703598022, 0.9419429898262024, 0.29091599583625793, -0.8260639905929565, 0.47304999828338623, -0.0134699996560812, -0.6006280183792114, 0.7822970151901245, -0.1611420065164566, -0.8495870232582092, 0.4440779983997345, 0.17417700588703156, -0.5251449942588806, 0.8236340284347534, -0.21412399411201477, -0.9991480112075806, 0.0017519999528303742, 0.007890000008046627, -0.9946579933166504, 0.06129400059580803, 0.007796999998390675, -0.9840919971466064, 0.008732999674975872, -0.0001289999927394092, -0.9916059970855713, 0.015207000076770782, -0.04798699915409088, -0.9899899959564209, -0.13816699385643005, -0.019433999434113503, -0.7927820086479187, -0.5669599771499634, 0.06795799732208252, -0.8363490104675293, -0.4685719907283783, 0.048955000936985016, -0.8138830065727234, -0.4743089973926544, 0.0008379999781027436, -0.8869869709014893, -0.4417180120944977, -0.05625399947166443, -0.7898640036582947, -0.5522750020027161, -0.15016800165176392, -0.297340989112854, -0.8998129963874817, -0.3192580044269562, -0.49759799242019653, -0.8317790031433105, -0.24411599338054657, -0.6295620203018188, -0.7765420079231262, 0.01261799968779087, -0.011338000185787678, -0.9998990297317505, -0.008561000227928162, -0.3547320067882538, -0.8679590225219727, -0.3453510105609894, 0.09618999809026718, 0.49066001176834106, -0.5, 0.1851000040769577, 0.27721700072288513, -0.6666669845581055, 0.32566601037979126, 0.76139897108078, -0.18199099600315094, 0.062401000410318375, 0.9939020276069641, -0.09090700000524521, 0.3803209960460663, 0.9214360117912292, -0.00007100000220816582, 0.030918000265955925, 0.9969729781150818, 0.07133600115776062, 0.3804109990596771, 0.9220889806747437, 0.0001630000042496249, 0.02471200004220009, 0.9975799918174744, 0.06498300284147263, 0.35510900616645813, 0.926891028881073, 0.03216100111603737, 0.07657899707555771, 0.9924740195274353, -0.09555599838495255, 0.27721700072288513, 0.1851000040769577, -0.6666669845581055, 0.5929989814758301, 0.5781109929084778, -0.18205299973487854, 0.7048519849777222, 0.7048519849777222, 0, 0.7052720189094543, 0.7054179906845093, -0.00002499999936844688, 0.6835219860076904, 0.7199410200119019, 0.03204600140452385, 0.3271070122718811, 0.06412599980831146, -0.6666669845581055, 0.7694699764251709, 0.3061000108718872, -0.18225300312042236, 0.9214379787445068, 0.38033199310302734, 0.0000670000008540228, 0.9220880270004272, 0.3804430067539215, -0.00016799999866634607, 0.9071130156517029, 0.403003990650177, 0.032437000423669815, 0, 0, -1, 0.6626030206680298, 0.04157499969005585, -0.272724986076355, 0.9969789981842041, 0.03082600049674511, -0.07129299640655518, 0.9975910186767578, 0.024447999894618988, -0.06492199748754501, 0.9925040006637573, 0.07630900293588638, 0.09545700252056122, 0.49066001176834106, -0.09618999809026718, -0.5, 0.27721700072288513, -0.1851000040769577, -0.6666669845581055, 0.76139897108078, -0.32566601037979126, -0.18199099600315094, 0.9939020276069641, -0.062401000410318375, -0.09090700000524521, 0.9214360117912292, -0.3803209960460663, -0.00007100000220816582, 0.9969729781150818, -0.030918000265955925, 0.07133600115776062, 0.9220889806747437, -0.3804109990596771, 0.0001630000042496249, 0.9975799918174744, -0.02471200004220009, 0.06498300284147263, 0.926891028881073, -0.35510900616645813, 0.03216100111603737, 0.9924740195274353, -0.07657899707555771, -0.09555599838495255, 0.1851000040769577, -0.27721700072288513, -0.6666669845581055, 0.5781109929084778, -0.5929989814758301, -0.18205299973487854, 0.7048519849777222, -0.7048519849777222, 0, 0.7054179906845093, -0.7052720189094543, -0.00002499999936844688, 0.7199410200119019, -0.6835219860076904, 0.03204600140452385, 0.06412599980831146, -0.3271070122718811, -0.6666669845581055, 0.3061000108718872, -0.7694699764251709, -0.18225300312042236, 0.38033199310302734, -0.9214379787445068, 0.0000670000008540228, 0.3804430067539215, -0.9220880270004272, -0.00016799999866634607, 0.403003990650177, -0.9071130156517029, 0.032437000423669815, 0, 0, -1, 0.04157499969005585, -0.6626030206680298, -0.272724986076355, 0.03082600049674511, -0.9969789981842041, -0.07129299640655518, 0.024447999894618988, -0.9975910186767578, -0.06492199748754501, 0.07630900293588638, -0.9925040006637573, 0.09545700252056122, -0.09618999809026718, -0.49066001176834106, -0.5, -0.1851000040769577, -0.27721700072288513, -0.6666669845581055, -0.32566601037979126, -0.76139897108078, -0.18199099600315094, -0.062401000410318375, -0.9939020276069641, -0.09090700000524521, -0.3803209960460663, -0.9214360117912292, -0.00007100000220816582, -0.030918000265955925, -0.9969729781150818, 0.07133600115776062, -0.3804109990596771, -0.9220889806747437, 0.0001630000042496249, -0.02471200004220009, -0.9975799918174744, 0.06498300284147263, -0.35510900616645813, -0.926891028881073, 0.03216100111603737, -0.07657899707555771, -0.9924740195274353, -0.09555599838495255, -0.27721700072288513, -0.1851000040769577, -0.6666669845581055, -0.5929989814758301, -0.5781109929084778, -0.18205299973487854, -0.7048519849777222, -0.7048519849777222, 0, -0.7052720189094543, -0.7054179906845093, -0.00002499999936844688, -0.6835219860076904, -0.7199410200119019, 0.03204600140452385, -0.3271070122718811, -0.06412599980831146, -0.6666669845581055, -0.7694699764251709, -0.3061000108718872, -0.18225300312042236, -0.9214379787445068, -0.38033199310302734, 0.0000670000008540228, -0.9220880270004272, -0.3804430067539215, -0.00016799999866634607, -0.9071130156517029, -0.403003990650177, 0.032437000423669815, 0, 0, -1, -0.6626030206680298, -0.04157499969005585, -0.272724986076355, -0.9969789981842041, -0.03082600049674511, -0.07129299640655518, -0.9975910186767578, -0.024447999894618988, -0.06492199748754501, -0.9925040006637573, -0.07630900293588638, 0.09545700252056122, -0.49066001176834106, 0.09618999809026718, -0.5, -0.27721700072288513, 0.1851000040769577, -0.6666669845581055, -0.76139897108078, 0.32566601037979126, -0.18199099600315094, -0.9939020276069641, 0.062401000410318375, -0.09090700000524521, -0.9214360117912292, 0.3803209960460663, -0.00007100000220816582, -0.9969729781150818, 0.030918000265955925, 0.07133600115776062, -0.9220889806747437, 0.3804109990596771, 0.0001630000042496249, -0.9975799918174744, 0.02471200004220009, 0.06498300284147263, -0.926891028881073, 0.35510900616645813, 0.03216100111603737, -0.9924740195274353, 0.07657899707555771, -0.09555599838495255, -0.1851000040769577, 0.27721700072288513, -0.6666669845581055, -0.5781109929084778, 0.5929989814758301, -0.18205299973487854, -0.7048519849777222, 0.7048519849777222, 0, -0.7054179906845093, 0.7052720189094543, -0.00002499999936844688, -0.7199410200119019, 0.6835219860076904, 0.03204600140452385, -0.06412599980831146, 0.3271070122718811, -0.6666669845581055, -0.3061000108718872, 0.7694699764251709, -0.18225300312042236, -0.38033199310302734, 0.9214379787445068, 0.0000670000008540228, -0.3804430067539215, 0.9220880270004272, -0.00016799999866634607, -0.403003990650177, 0.9071130156517029, 0.032437000423669815, 0, 0, -1, -0.04157499969005585, 0.6626030206680298, -0.272724986076355, -0.03082600049674511, 0.9969789981842041, -0.07129299640655518, -0.024447999894618988, 0.9975910186767578, -0.06492199748754501, -0.07630900293588638, 0.9925040006637573, 0.09545700252056122, 0.07657899707555771, 0.9924740195274353, -0.09555599838495255, 0.40307098627090454, 0.9070649743080139, -0.03255299851298332, 0.3753640055656433, 0.9070209860801697, 0.000007000000096013537, 0.18306200206279755, 0.9820899963378906, -0.04457399994134903, 0.3751649856567383, 0.9065750241279602, -0.00007400000322377309, 0.18801499903202057, 0.9816100001335144, -0.03304100036621094, 0.3759070038795471, 0.908607006072998, -0.00026199998683296144, 0.16623400151729584, 0.983722984790802, -0.06822899729013443, 0.33324098587036133, 0.9290030002593994, 0.029803000390529633, 0.14071400463581085, 0.9862040281295776, -0.08718100190162659, 0.7198299765586853, 0.6836559772491455, -0.032017000019550323, 0.6943539977073669, 0.6943539977073669, 0, 0.694034993648529, 0.694034993648529, 0, 0.6955100297927856, 0.6955100297927856, 0, 0.6639170050621033, 0.7306150197982788, 0.029100999236106873, 0.9268649816513062, 0.35523301362991333, -0.03203999996185303, 0.9070209860801697, 0.3753649890422821, -0.000007000000096013537, 0.9065750241279602, 0.3751649856567383, 0.00007300000288523734, 0.908607006072998, 0.3759070038795471, 0.00026199998683296144, 0.8926259875297546, 0.4211460053920746, 0.028991999104619026, 0.9924740195274353, 0.07646500319242477, 0.09565100073814392, 0.9820899963378906, 0.18306200206279755, 0.04457399994134903, 0.9816100001335144, 0.18801499903202057, 0.03304100036621094, 0.983722984790802, 0.16623400151729584, 0.06822899729013443, 0.9862040281295776, 0.14071400463581085, 0.08718100190162659, 0.9924740195274353, -0.07657899707555771, -0.09555599838495255, 0.9070649743080139, -0.40307098627090454, -0.03255299851298332, 0.9070209860801697, -0.3753640055656433, 0.000007000000096013537, 0.9820899963378906, -0.18306200206279755, -0.04457399994134903, 0.9065750241279602, -0.3751649856567383, -0.00007400000322377309, 0.9816100001335144, -0.18801499903202057, -0.03304100036621094, 0.908607006072998, -0.3759070038795471, -0.00026199998683296144, 0.983722984790802, -0.16623400151729584, -0.06822899729013443, 0.9290030002593994, -0.33324098587036133, 0.029803000390529633, 0.9862040281295776, -0.14071400463581085, -0.08718100190162659, 0.6836559772491455, -0.7198299765586853, -0.032017000019550323, 0.6943539977073669, -0.6943539977073669, 0, 0.694034993648529, -0.694034993648529, 0, 0.6955100297927856, -0.6955100297927856, 0, 0.7306150197982788, -0.6639170050621033, 0.029100999236106873, 0.35523301362991333, -0.9268649816513062, -0.03203999996185303, 0.3753649890422821, -0.9070209860801697, -0.000007000000096013537, 0.3751649856567383, -0.9065750241279602, 0.00007300000288523734, 0.3759070038795471, -0.908607006072998, 0.00026199998683296144, 0.4211460053920746, -0.8926259875297546, 0.028991999104619026, 0.07646500319242477, -0.9924740195274353, 0.09565100073814392, 0.18306200206279755, -0.9820899963378906, 0.04457399994134903, 0.18801499903202057, -0.9816100001335144, 0.03304100036621094, 0.16623400151729584, -0.983722984790802, 0.06822899729013443, 0.14071400463581085, -0.9862040281295776, 0.08718100190162659, -0.07657899707555771, -0.9924740195274353, -0.09555599838495255, -0.40307098627090454, -0.9070649743080139, -0.03255299851298332, -0.3753640055656433, -0.9070209860801697, 0.000007000000096013537, -0.18306200206279755, -0.9820899963378906, -0.04457399994134903, -0.3751649856567383, -0.9065750241279602, -0.00007400000322377309, -0.18801499903202057, -0.9816100001335144, -0.03304100036621094, -0.3759070038795471, -0.908607006072998, -0.00026199998683296144, -0.16623400151729584, -0.983722984790802, -0.06822899729013443, -0.33324098587036133, -0.9290030002593994, 0.029803000390529633, -0.14071400463581085, -0.9862040281295776, -0.08718100190162659, -0.7198299765586853, -0.6836559772491455, -0.032017000019550323, -0.6943539977073669, -0.6943539977073669, 0, -0.694034993648529, -0.694034993648529, 0, -0.6955100297927856, -0.6955100297927856, 0, -0.6639170050621033, -0.7306150197982788, 0.029100999236106873, -0.9268649816513062, -0.35523301362991333, -0.03203999996185303, -0.9070209860801697, -0.3753649890422821, -0.000007000000096013537, -0.9065750241279602, -0.3751649856567383, 0.00007300000288523734, -0.908607006072998, -0.3759070038795471, 0.00026199998683296144, -0.8926259875297546, -0.4211460053920746, 0.028991999104619026, -0.9924740195274353, -0.07646500319242477, 0.09565100073814392, -0.9820899963378906, -0.18306200206279755, 0.04457399994134903, -0.9816100001335144, -0.18801499903202057, 0.03304100036621094, -0.983722984790802, -0.16623400151729584, 0.06822899729013443, -0.9862040281295776, -0.14071400463581085, 0.08718100190162659, -0.9924740195274353, 0.07657899707555771, -0.09555599838495255, -0.9070649743080139, 0.40307098627090454, -0.03255299851298332, -0.9070209860801697, 0.3753640055656433, 0.000007000000096013537, -0.9820899963378906, 0.18306200206279755, -0.04457399994134903, -0.9065750241279602, 0.3751649856567383, -0.00007400000322377309, -0.9816100001335144, 0.18801499903202057, -0.03304100036621094, -0.908607006072998, 0.3759070038795471, -0.00026199998683296144, -0.983722984790802, 0.16623400151729584, -0.06822899729013443, -0.9290030002593994, 0.33324098587036133, 0.029803000390529633, -0.9862040281295776, 0.14071400463581085, -0.08718100190162659, -0.6836559772491455, 0.7198299765586853, -0.032017000019550323, -0.6943539977073669, 0.6943539977073669, 0, -0.694034993648529, 0.694034993648529, 0, -0.6955100297927856, 0.6955100297927856, 0, -0.7306150197982788, 0.6639170050621033, 0.029100999236106873, -0.35523301362991333, 0.9268649816513062, -0.03203999996185303, -0.3753649890422821, 0.9070209860801697, -0.000007000000096013537, -0.3751649856567383, 0.9065750241279602, 0.00007300000288523734, -0.3759070038795471, 0.908607006072998, 0.00026199998683296144, -0.4211460053920746, 0.8926259875297546, 0.028991999104619026, -0.07646500319242477, 0.9924740195274353, 0.09565100073814392, -0.18306200206279755, 0.9820899963378906, 0.04457399994134903, -0.18801499903202057, 0.9816100001335144, 0.03304100036621094, -0.16623400151729584, 0.983722984790802, 0.06822899729013443, -0.14071400463581085, 0.9862040281295776, 0.08718100190162659 ]); var teapotBinormals = new Float32Array([ 0.2554270029067993, -0.05043400079011917, -0.9655119776725769, 0.2302899956703186, -0.11379700154066086, -0.9664459824562073, -0.23653900623321533, 0.09789499640464783, -0.9666780233383179, -0.2551180124282837, 0.05037299916148186, -0.9655969738960266, -0.9201610088348389, 0.38079801201820374, -0.09108299762010574, -0.9770479798316956, 0.1929199993610382, -0.09032399952411652, -0.6762400269508362, 0.2798590064048767, 0.6814529895782471, -0.723800003528595, 0.1429159939289093, 0.6750479936599731, -0.4681990146636963, 0.1581760048866272, 0.869350016117096, -0.4902079999446869, 0.09679199755191803, 0.8662149906158447, 0.16952399909496307, -0.1934960037469864, -0.9663439989089966, -0.18106800317764282, 0.18106800317764282, -0.9666590094566345, -0.7041199803352356, 0.7041199803352356, -0.09181900322437286, -0.5179349780082703, 0.5179349780082703, 0.6807990074157715, -0.37217798829078674, 0.3260670006275177, 0.8690019845962524, 0.08221600204706192, -0.243368998169899, -0.9664430022239685, -0.09789499640464783, 0.23653900623321533, -0.9666780233383179, -0.38079801201820374, 0.9201610088348389, -0.09108199924230576, -0.2798590064048767, 0.6762400269508362, 0.6814540028572083, -0.21894000470638275, 0.44305500388145447, 0.8693490028381348, 0.050822000950574875, -0.2573910057544708, -0.9649699926376343, -0.05021600052714348, 0.25432100892066956, -0.965815007686615, -0.19291600584983826, 0.9770249724388123, -0.09059000015258789, -0.14291299879550934, 0.7237870097160339, 0.6750609874725342, -0.09679199755191803, 0.4902079999446869, 0.8662149906158447, -0.048507001250982285, -0.2576940059661865, -0.965008020401001, -0.15833300352096558, -0.3227809965610504, -0.933135986328125, 0.05656199902296066, 0.13793900609016418, -0.9888240098953247, 0.049150001257658005, 0.2545199990272522, -0.9658179879188538, 0.378387987613678, 0.9173290133476257, -0.12381099909543991, 0.1917950063943863, 0.9772530198097229, -0.09050799906253815, 0.2777239978313446, 0.6716070175170898, 0.6868870258331299, 0.14281700551509857, 0.7238019704818726, 0.6750659942626953, 0.15788200497627258, 0.4674209952354431, 0.8698220252990723, 0.09679199755191803, 0.4902079999446869, 0.8662149906158447, -0.3139069974422455, -0.2657270133495331, -0.9115110039710999, 0.05247500166296959, 0.05178600177168846, -0.9972789883613586, 0.699787974357605, 0.6969379782676697, -0.15676100552082062, 0.511929988861084, 0.5116159915924072, 0.6900550127029419, 0.32515400648117065, 0.37111398577690125, 0.8697980046272278, -0.3181929886341095, -0.09987600147724152, -0.9427499771118164, 0.1552799940109253, 0.06176299974322319, -0.9859380125999451, 0.9187250137329102, 0.3751460015773773, -0.1233299970626831, 0.6724870204925537, 0.2775439918041229, 0.6860979795455933, 0.4424299895763397, 0.21853800117969513, 0.8697689771652222, -0.255948007106781, -0.04464200139045715, -0.9656590223312378, 0.25306200981140137, 0.046362001448869705, -0.9663389921188354, 0.9778940081596375, 0.18800100684165955, -0.09153299778699875, 0.7238150238990784, 0.14205799996852875, 0.675212025642395, 0.49017900228500366, 0.0967010036110878, 0.8662409782409668, -0.25491899251937866, 0.05033399909734726, -0.9656509757041931, -0.2302899956703186, 0.11379700154066086, -0.9664459824562073, 0.23653900623321533, -0.09789499640464783, -0.9666780233383179, 0.252265989780426, -0.04980999976396561, -0.9663749933242798, 0.9201610088348389, -0.38079801201820374, -0.09108299762010574, 0.9769039750099182, -0.19289200007915497, -0.09193000197410583, 0.6762400269508362, -0.2798590064048767, 0.6814529895782471, 0.7236610054969788, -0.14288799464702606, 0.6752020120620728, 0.4681990146636963, -0.1581760048866272, 0.869350016117096, 0.4901660084724426, -0.09678400307893753, 0.8662390112876892, -0.16952399909496307, 0.1934960037469864, -0.9663439989089966, 0.18106800317764282, -0.18106800317764282, -0.9666590094566345, 0.7041199803352356, -0.7041199803352356, -0.09181900322437286, 0.5179349780082703, -0.5179349780082703, 0.6807990074157715, 0.37217798829078674, -0.3260670006275177, 0.8690019845962524, -0.08221600204706192, 0.243368998169899, -0.9664430022239685, 0.09789499640464783, -0.23653900623321533, -0.9666780233383179, 0.38079801201820374, -0.9201610088348389, -0.09108199924230576, 0.2798590064048767, -0.6762400269508362, 0.6814540028572083, 0.21894000470638275, -0.44305500388145447, 0.8693490028381348, -0.05043400079011917, 0.2554270029067993, -0.9655119776725769, 0.05037299916148186, -0.2551180124282837, -0.9655969738960266, 0.1929199993610382, -0.9770479798316956, -0.09032399952411652, 0.1429159939289093, -0.723800003528595, 0.6750479936599731, 0.09679199755191803, -0.4902079999446869, 0.8662149906158447, 0.05043400079011917, 0.2554270029067993, -0.9655119776725769, 0.11379700154066086, 0.2302899956703186, -0.9664459824562073, -0.09789499640464783, -0.23653900623321533, -0.9666780233383179, -0.05037299916148186, -0.2551180124282837, -0.9655969738960266, -0.38079801201820374, -0.9201610088348389, -0.09108299762010574, -0.1929199993610382, -0.9770479798316956, -0.09032399952411652, -0.2798590064048767, -0.6762400269508362, 0.6814529895782471, -0.1429159939289093, -0.723800003528595, 0.6750479936599731, -0.1581760048866272, -0.4681990146636963, 0.869350016117096, -0.09679199755191803, -0.4902079999446869, 0.8662149906158447, 0.1934960037469864, 0.16952399909496307, -0.9663439989089966, -0.18106800317764282, -0.18106800317764282, -0.9666590094566345, -0.7041199803352356, -0.7041199803352356, -0.09181900322437286, -0.5179349780082703, -0.5179349780082703, 0.6807990074157715, -0.3260670006275177, -0.37217798829078674, 0.8690019845962524, 0.243368998169899, 0.08221600204706192, -0.9664430022239685, -0.23653900623321533, -0.09789499640464783, -0.9666780233383179, -0.9201610088348389, -0.38079801201820374, -0.09108199924230576, -0.6762400269508362, -0.2798590064048767, 0.6814540028572083, -0.44305500388145447, -0.21894000470638275, 0.8693490028381348, 0.2554270029067993, 0.05043400079011917, -0.9655119776725769, -0.2551180124282837, -0.05037299916148186, -0.9655969738960266, -0.9770479798316956, -0.1929199993610382, -0.09032399952411652, -0.723800003528595, -0.1429159939289093, 0.6750479936599731, -0.4902079999446869, -0.09679199755191803, 0.8662149906158447, -0.4902079999446869, 0.09679199755191803, 0.8662149906158447, -0.44305500388145447, 0.21893900632858276, 0.8693490028381348, -0.37287598848342896, 0.15431199967861176, 0.9149600267410278, -0.4014579951763153, 0.07926800101995468, 0.9124410152435303, -0.3112579882144928, 0.12881100177764893, 0.9415550231933594, -0.33541300892829895, 0.0662280023097992, 0.939740002155304, -0.19015200436115265, 0.07869099825620651, 0.9785959720611572, -0.20517399907112122, 0.040511999279260635, 0.977886974811554, 0.06301800161600113, -0.021289000287652016, 0.9977849721908569, 0.06623400002717972, -0.013078000396490097, 0.9977179765701294, -0.3260670006275177, 0.37217798829078674, 0.8690019845962524, -0.285739004611969, 0.285739004611969, 0.9147170186042786, -0.23854400217533112, 0.23854400217533112, 0.9413790106773376, -0.14574900269508362, 0.14574900269508362, 0.9785270094871521, 0.05011200159788132, -0.04390300065279007, 0.9977779984474182, -0.1581760048866272, 0.4681999981403351, 0.869350016117096, -0.15431199967861176, 0.37287598848342896, 0.9149600267410278, -0.12881100177764893, 0.3112579882144928, 0.9415550231933594, -0.07869099825620651, 0.19015100598335266, 0.9785959720611572, 0.02946699969470501, -0.05963199958205223, 0.9977849721908569, -0.09679199755191803, 0.4902079999446869, 0.8662149906158447, -0.07926800101995468, 0.4014579951763153, 0.9124410152435303, -0.0662280023097992, 0.33541300892829895, 0.939740002155304, -0.040511999279260635, 0.20517399907112122, 0.977886974811554, 0.013078000396490097, -0.06623400002717972, 0.9977179765701294, 0.09679199755191803, 0.4902079999446869, 0.8662149906158447, 0.21858200430870056, 0.4423219859600067, 0.86981201171875, 0.15431199967861176, 0.37287598848342896, 0.9149600267410278, 0.07926800101995468, 0.4014579951763153, 0.9124410152435303, 0.12881100177764893, 0.3112579882144928, 0.9415550231933594, 0.0662280023097992, 0.33541300892829895, 0.939740002155304, 0.07869099825620651, 0.19015200436115265, 0.9785959720611572, 0.040511999279260635, 0.20517399907112122, 0.977886974811554, -0.021289000287652016, -0.06301800161600113, 0.9977849721908569, -0.013078000396490097, -0.06623400002717972, 0.9977179765701294, 0.3711329996585846, 0.3251489996910095, 0.8697919845581055, 0.285739004611969, 0.285739004611969, 0.9147170186042786, 0.23854400217533112, 0.23854400217533112, 0.9413790106773376, 0.14574900269508362, 0.14574900269508362, 0.9785270094871521, -0.04390300065279007, -0.05011200159788132, 0.9977779984474182, 0.46750199794769287, 0.15794099867343903, 0.8697680234909058, 0.37287598848342896, 0.15431199967861176, 0.9149600267410278, 0.3112579882144928, 0.12881100177764893, 0.9415550231933594, 0.19015100598335266, 0.07869099825620651, 0.9785959720611572, -0.05963199958205223, -0.02946699969470501, 0.9977849721908569, 0.49017900228500366, 0.0967010036110878, 0.8662409782409668, 0.4014579951763153, 0.07926800101995468, 0.9124410152435303, 0.33541300892829895, 0.0662280023097992, 0.939740002155304, 0.20517399907112122, 0.040511999279260635, 0.977886974811554, -0.06623400002717972, -0.013078000396490097, 0.9977179765701294, 0.4901660084724426, -0.09678400307893753, 0.8662390112876892, 0.44305500388145447, -0.21893900632858276, 0.8693490028381348, 0.37287598848342896, -0.15431199967861176, 0.9149600267410278, 0.4014579951763153, -0.07926800101995468, 0.9124410152435303, 0.3112579882144928, -0.12881100177764893, 0.9415550231933594, 0.33541300892829895, -0.0662280023097992, 0.939740002155304, 0.19015200436115265, -0.07869099825620651, 0.9785959720611572, 0.20517399907112122, -0.040511999279260635, 0.977886974811554, -0.06301800161600113, 0.021289000287652016, 0.9977849721908569, -0.06623400002717972, 0.013078000396490097, 0.9977179765701294, 0.3260670006275177, -0.37217798829078674, 0.8690019845962524, 0.285739004611969, -0.285739004611969, 0.9147170186042786, 0.23854400217533112, -0.23854400217533112, 0.9413790106773376, 0.14574900269508362, -0.14574900269508362, 0.9785270094871521, -0.05011200159788132, 0.04390300065279007, 0.9977779984474182, 0.1581760048866272, -0.4681999981403351, 0.869350016117096, 0.15431199967861176, -0.37287598848342896, 0.9149600267410278, 0.12881100177764893, -0.3112579882144928, 0.9415550231933594, 0.07869099825620651, -0.19015100598335266, 0.9785959720611572, -0.02946699969470501, 0.05963199958205223, 0.9977849721908569, 0.09679199755191803, -0.4902079999446869, 0.8662149906158447, 0.07926800101995468, -0.4014579951763153, 0.9124410152435303, 0.0662280023097992, -0.33541300892829895, 0.939740002155304, 0.040511999279260635, -0.20517399907112122, 0.977886974811554, -0.013078000396490097, 0.06623400002717972, 0.9977179765701294, -0.09679199755191803, -0.4902079999446869, 0.8662149906158447, -0.21893900632858276, -0.44305500388145447, 0.8693490028381348, -0.15431199967861176, -0.37287598848342896, 0.9149600267410278, -0.07926800101995468, -0.4014579951763153, 0.9124410152435303, -0.12881100177764893, -0.3112579882144928, 0.9415550231933594, -0.0662280023097992, -0.33541300892829895, 0.939740002155304, -0.07869099825620651, -0.19015200436115265, 0.9785959720611572, -0.040511999279260635, -0.20517399907112122, 0.977886974811554, 0.021289000287652016, 0.06301800161600113, 0.9977849721908569, 0.013078000396490097, 0.06623400002717972, 0.9977179765701294, -0.37217798829078674, -0.3260670006275177, 0.8690019845962524, -0.285739004611969, -0.285739004611969, 0.9147170186042786, -0.23854400217533112, -0.23854400217533112, 0.9413790106773376, -0.14574900269508362, -0.14574900269508362, 0.9785270094871521, 0.04390300065279007, 0.05011200159788132, 0.9977779984474182, -0.4681999981403351, -0.1581760048866272, 0.869350016117096, -0.37287598848342896, -0.15431199967861176, 0.9149600267410278, -0.3112579882144928, -0.12881100177764893, 0.9415550231933594, -0.19015100598335266, -0.07869099825620651, 0.9785959720611572, 0.05963199958205223, 0.02946699969470501, 0.9977849721908569, -0.4902079999446869, -0.09679199755191803, 0.8662149906158447, -0.4014579951763153, -0.07926800101995468, 0.9124410152435303, -0.33541300892829895, -0.0662280023097992, 0.939740002155304, -0.20517399907112122, -0.040511999279260635, 0.977886974811554, 0.06623400002717972, 0.013078000396490097, 0.9977179765701294, 0.06623400002717972, -0.013078000396490097, 0.9977179765701294, 0.05963199958205223, -0.02946699969470501, 0.9977849721908569, 0.40303200483322144, -0.1667889952659607, 0.8998590111732483, 0.4339120090007782, -0.08567699790000916, 0.8968719840049744, 0.6326310038566589, -0.2618109881877899, 0.7288579940795898, 0.6777120232582092, -0.13381600379943848, 0.723048985004425, 0.6661339998245239, -0.27567601203918457, 0.6930140256881714, 0.7128540277481079, -0.14075499773025513, 0.6870430111885071, 0.5777599811553955, -0.19519099593162537, 0.792523980140686, 0.6036490201950073, -0.11919199675321579, 0.7882900238037109, 0.04390300065279007, -0.05011200159788132, 0.9977779984474182, 0.30884799361228943, -0.30884799361228943, 0.8995699882507324, 0.48457300662994385, -0.48457300662994385, 0.7282710075378418, 0.510138988494873, -0.510138988494873, 0.6924710273742676, 0.4591110050678253, -0.40222999453544617, 0.7921029925346375, 0.021289000287652016, -0.06301800161600113, 0.9977849721908569, 0.16678999364376068, -0.40303200483322144, 0.8998590111732483, 0.2618109881877899, -0.6326310038566589, 0.7288579940795898, 0.27567601203918457, -0.6661339998245239, 0.6930140256881714, 0.2701770067214966, -0.546737015247345, 0.7925170063972473, 0.013078000396490097, -0.06623400002717972, 0.9977179765701294, 0.08567599952220917, -0.4339120090007782, 0.8968719840049744, 0.13381600379943848, -0.6777120232582092, 0.723048985004425, 0.14075499773025513, -0.7128540277481079, 0.6870430111885071, 0.11919199675321579, -0.6036490201950073, 0.7882900238037109, -0.013078000396490097, -0.06623400002717972, 0.9977179765701294, -0.02946699969470501, -0.05963199958205223, 0.9977849721908569, -0.1667889952659607, -0.40303200483322144, 0.8998590111732483, -0.08567699790000916, -0.4339120090007782, 0.8968719840049744, -0.2618109881877899, -0.6326310038566589, 0.7288579940795898, -0.13381600379943848, -0.6777120232582092, 0.723048985004425, -0.27567601203918457, -0.6661339998245239, 0.6930140256881714, -0.14075499773025513, -0.7128540277481079, 0.6870430111885071, -0.19519099593162537, -0.5777599811553955, 0.792523980140686, -0.11919199675321579, -0.6036490201950073, 0.7882900238037109, -0.05011200159788132, -0.04390300065279007, 0.9977779984474182, -0.30884799361228943, -0.30884799361228943, 0.8995699882507324, -0.48457300662994385, -0.48457300662994385, 0.7282710075378418, -0.510138988494873, -0.510138988494873, 0.6924710273742676, -0.40222999453544617, -0.4591110050678253, 0.7921029925346375, -0.06301800161600113, -0.021289000287652016, 0.9977849721908569, -0.40303200483322144, -0.16678999364376068, 0.8998590111732483, -0.6326310038566589, -0.2618109881877899, 0.7288579940795898, -0.6661339998245239, -0.27567601203918457, 0.6930140256881714, -0.546737015247345, -0.2701770067214966, 0.7925170063972473, -0.06623400002717972, -0.013078000396490097, 0.9977179765701294, -0.4339120090007782, -0.08567599952220917, 0.8968719840049744, -0.6777120232582092, -0.13381600379943848, 0.723048985004425, -0.7128540277481079, -0.14075499773025513, 0.6870430111885071, -0.6036490201950073, -0.11919199675321579, 0.7882900238037109, -0.06623400002717972, 0.013078000396490097, 0.9977179765701294, -0.05963199958205223, 0.02946699969470501, 0.9977849721908569, -0.40303200483322144, 0.1667889952659607, 0.8998590111732483, -0.4339120090007782, 0.08567699790000916, 0.8968719840049744, -0.6326310038566589, 0.2618109881877899, 0.7288579940795898, -0.6777120232582092, 0.13381600379943848, 0.723048985004425, -0.6661339998245239, 0.27567601203918457, 0.6930140256881714, -0.7128540277481079, 0.14075499773025513, 0.6870430111885071, -0.5777599811553955, 0.19519099593162537, 0.792523980140686, -0.6036490201950073, 0.11919199675321579, 0.7882900238037109, -0.04390300065279007, 0.05011200159788132, 0.9977779984474182, -0.30884799361228943, 0.30884799361228943, 0.8995699882507324, -0.48457300662994385, 0.48457300662994385, 0.7282710075378418, -0.510138988494873, 0.510138988494873, 0.6924710273742676, -0.4591110050678253, 0.40222999453544617, 0.7921029925346375, -0.021289000287652016, 0.06301800161600113, 0.9977849721908569, -0.16678999364376068, 0.40303200483322144, 0.8998590111732483, -0.2618109881877899, 0.6326310038566589, 0.7288579940795898, -0.27567601203918457, 0.6661339998245239, 0.6930140256881714, -0.2701770067214966, 0.546737015247345, 0.7925170063972473, -0.013078000396490097, 0.06623400002717972, 0.9977179765701294, -0.08567599952220917, 0.4339120090007782, 0.8968719840049744, -0.13381600379943848, 0.6777120232582092, 0.723048985004425, -0.14075499773025513, 0.7128540277481079, 0.6870430111885071, -0.11919199675321579, 0.6036490201950073, 0.7882900238037109, 0.013078000396490097, 0.06623400002717972, 0.9977179765701294, 0.02946699969470501, 0.05963199958205223, 0.9977849721908569, 0.1667889952659607, 0.40303200483322144, 0.8998590111732483, 0.08567699790000916, 0.4339120090007782, 0.8968719840049744, 0.2618109881877899, 0.6326310038566589, 0.7288579940795898, 0.13381600379943848, 0.6777120232582092, 0.723048985004425, 0.27567601203918457, 0.6661339998245239, 0.6930140256881714, 0.14075499773025513, 0.7128540277481079, 0.6870430111885071, 0.19519099593162537, 0.5777599811553955, 0.792523980140686, 0.11919199675321579, 0.6036490201950073, 0.7882900238037109, 0.05011200159788132, 0.04390300065279007, 0.9977779984474182, 0.30884799361228943, 0.30884799361228943, 0.8995699882507324, 0.48457300662994385, 0.48457300662994385, 0.7282710075378418, 0.510138988494873, 0.510138988494873, 0.6924710273742676, 0.40222999453544617, 0.4591110050678253, 0.7921029925346375, 0.06301800161600113, 0.021289000287652016, 0.9977849721908569, 0.40303200483322144, 0.16678999364376068, 0.8998590111732483, 0.6326310038566589, 0.2618109881877899, 0.7288579940795898, 0.6661339998245239, 0.27567601203918457, 0.6930140256881714, 0.546737015247345, 0.2701770067214966, 0.7925170063972473, 0.06623400002717972, 0.013078000396490097, 0.9977179765701294, 0.4339120090007782, 0.08567599952220917, 0.8968719840049744, 0.6777120232582092, 0.13381600379943848, 0.723048985004425, 0.7128540277481079, 0.14075499773025513, 0.6870430111885071, 0.6036490201950073, 0.11919199675321579, 0.7882900238037109, 0.6036490201950073, -0.11919199675321579, 0.7882900238037109, 0.546737015247345, -0.2701770067214966, 0.7925170063972473, 0.7223830223083496, -0.2989569902420044, 0.623528003692627, 0.7723940014839172, -0.15251100063323975, 0.616562008857727, 0.9094089865684509, -0.3763520121574402, 0.1770150065422058, 0.9660869836807251, -0.19075599312782288, 0.1740349978208542, 0.9408230185508728, -0.3353259861469269, 0.04907499998807907, 0.9843119978904724, -0.16964000463485718, 0.048493001610040665, 0.9810580015182495, -0.1937119960784912, 0, 0.7071070075035095, -0.7071070075035095, 0, 0.40222999453544617, -0.4591110050678253, 0.7921029925346375, 0.5532029867172241, -0.5532029867172241, 0.622842013835907, 0.6959879994392395, -0.6959879994392395, 0.17663900554180145, 0.7403979897499084, -0.6703829765319824, 0.048958998173475266, 0.8310419917106628, -0.5562090277671814, 0, 0.19519099593162537, -0.5777599811553955, 0.792523980140686, 0.2989560067653656, -0.7223830223083496, 0.623528003692627, 0.3763520121574402, -0.9094089865684509, 0.1770150065422058, 0.4275760054588318, -0.902646005153656, 0.04906899854540825, 0.5562090277671814, -0.8310419917106628, 0, 0.11919199675321579, -0.6036490201950073, 0.7882900238037109, 0.15251100063323975, -0.7723940014839172, 0.616562008857727, 0.19075599312782288, -0.9660869836807251, 0.1740349978208542, 0.19348600506782532, -0.9799140095710754, 0.048277001827955246, 0.1937119960784912, -0.9810580015182495, 0, -0.11919199675321579, -0.6036490201950073, 0.7882900238037109, -0.2701770067214966, -0.546737015247345, 0.7925170063972473, -0.2989569902420044, -0.7223830223083496, 0.623528003692627, -0.15251100063323975, -0.7723940014839172, 0.616562008857727, -0.3763520121574402, -0.9094089865684509, 0.1770150065422058, -0.19075599312782288, -0.9660869836807251, 0.1740349978208542, -0.3353259861469269, -0.9408230185508728, 0.04907499998807907, -0.16964000463485718, -0.9843119978904724, 0.04849399998784065, -0.1937119960784912, -0.9810580015182495, 0, 0.7071070075035095, -0.7071070075035095, 0, -0.4591110050678253, -0.40222999453544617, 0.7921029925346375, -0.5532029867172241, -0.5532029867172241, 0.622842013835907, -0.6959879994392395, -0.6959879994392395, 0.17663900554180145, -0.6703829765319824, -0.7403979897499084, 0.048958998173475266, -0.5562090277671814, -0.8310419917106628, 0, -0.5777599811553955, -0.19519099593162537, 0.792523980140686, -0.7223830223083496, -0.2989560067653656, 0.623528003692627, -0.9094089865684509, -0.3763520121574402, 0.1770150065422058, -0.902646005153656, -0.4275760054588318, 0.04906899854540825, -0.8310419917106628, -0.5562090277671814, 0, -0.6036490201950073, -0.11919199675321579, 0.7882900238037109, -0.7723940014839172, -0.15251100063323975, 0.616562008857727, -0.9660869836807251, -0.19075599312782288, 0.1740349978208542, -0.9799140095710754, -0.19348600506782532, 0.048277001827955246, -0.9810580015182495, -0.1937119960784912, 0, -0.6036490201950073, 0.11919199675321579, 0.7882900238037109, -0.546737015247345, 0.2701770067214966, 0.7925170063972473, -0.7223830223083496, 0.2989569902420044, 0.623528003692627, -0.7723940014839172, 0.15251100063323975, 0.616562008857727, -0.9094089865684509, 0.3763520121574402, 0.1770150065422058, -0.9660869836807251, 0.19075599312782288, 0.1740349978208542, -0.9408230185508728, 0.3353259861469269, 0.04907499998807907, -0.9843119978904724, 0.16964000463485718, 0.04849399998784065, -0.9810580015182495, 0.1937119960784912, 0, 0.7071070075035095, -0.7071070075035095, 0, -0.40222999453544617, 0.4591110050678253, 0.7921029925346375, -0.5532029867172241, 0.5532029867172241, 0.622842013835907, -0.6959879994392395, 0.6959879994392395, 0.17663900554180145, -0.7403979897499084, 0.6703829765319824, 0.048958998173475266, -0.8310419917106628, 0.5562090277671814, 0, -0.19519099593162537, 0.5777599811553955, 0.792523980140686, -0.2989560067653656, 0.7223830223083496, 0.623528003692627, -0.3763520121574402, 0.9094089865684509, 0.1770150065422058, -0.4275760054588318, 0.902646005153656, 0.04906899854540825, -0.5562090277671814, 0.8310419917106628, 0, -0.11919199675321579, 0.6036490201950073, 0.7882900238037109, -0.15251100063323975, 0.7723940014839172, 0.616562008857727, -0.19075599312782288, 0.9660869836807251, 0.1740349978208542, -0.19348600506782532, 0.9799140095710754, 0.048277001827955246, -0.1937119960784912, 0.9810580015182495, 0, 0.11919199675321579, 0.6036490201950073, 0.7882900238037109, 0.2701770067214966, 0.546737015247345, 0.7925170063972473, 0.2989569902420044, 0.7223830223083496, 0.623528003692627, 0.15251100063323975, 0.7723940014839172, 0.616562008857727, 0.3763520121574402, 0.9094089865684509, 0.1770150065422058, 0.19075599312782288, 0.9660869836807251, 0.1740349978208542, 0.3353259861469269, 0.9408230185508728, 0.04907499998807907, 0.16964000463485718, 0.9843119978904724, 0.04849399998784065, 0.1937119960784912, 0.9810580015182495, 0, 0.7071070075035095, -0.7071070075035095, 0, 0.4591110050678253, 0.40222999453544617, 0.7921029925346375, 0.5532029867172241, 0.5532029867172241, 0.622842013835907, 0.6959879994392395, 0.6959879994392395, 0.17663900554180145, 0.6703829765319824, 0.7403979897499084, 0.048958998173475266, 0.5562090277671814, 0.8310419917106628, 0, 0.5777599811553955, 0.19519099593162537, 0.792523980140686, 0.7223830223083496, 0.2989560067653656, 0.623528003692627, 0.9094089865684509, 0.3763520121574402, 0.1770150065422058, 0.902646005153656, 0.4275760054588318, 0.04906899854540825, 0.8310419917106628, 0.5562090277671814, 0, 0.6036490201950073, 0.11919199675321579, 0.7882900238037109, 0.7723940014839172, 0.15251100063323975, 0.616562008857727, 0.9660869836807251, 0.19075599312782288, 0.1740349978208542, 0.9799150228500366, 0.19348600506782532, 0.048277001827955246, 0.9810580015182495, 0.1937119960784912, 0, 0.9999480247497559, 0.006622000131756067, 0.007786999922245741, 0.9989290237426758, 0.04125700145959854, -0.020945999771356583, 0.9700260162353516, -0.18230900168418884, 0.1606609970331192, 0.9929869771003723, -0.1116809993982315, 0.038782998919487, 0.8677089810371399, -0.30993399024009705, 0.38861599564552307, 0.9675049781799316, -0.18179599940776825, 0.17574100196361542, 0.6127229928970337, -0.23797500133514404, 0.753616988658905, 0.781611979007721, -0.1585649996995926, 0.6032750010490417, 0.13049399852752686, -0.02585900016129017, 0.9911119937896729, 0.16583800315856934, -0.04606600105762482, 0.9850770235061646, 0.9847609996795654, -0.03004699945449829, -0.17129500210285187, 0.9463850259780884, 0.008138000033795834, 0.3229379951953888, 0.6942890286445618, 0.06011800095438957, 0.7171810269355774, 0.405923992395401, 0.09244199842214584, 0.9092199802398682, 0.1477230042219162, 0.0024739999789744616, 0.9890260100364685, 0.991798996925354, -0.11557900160551071, -0.05454900115728378, 0.9920099973678589, 0.07202500104904175, 0.10358300060033798, 0.8882240056991577, 0.2238840013742447, 0.40116599202156067, 0.6046879887580872, 0.13691100478172302, 0.7846069931983948, 0.12908099591732025, -0.06111999973654747, 0.989749014377594, 0.9954820275306702, -0.09436299651861191, 0.010502999648451805, 0.9981970191001892, 0.012060999870300293, 0.05880200117826462, 0.9550639986991882, 0.09819000214338303, 0.27966299653053284, 0.6606940031051636, 0.05169999971985817, 0.7488729953765869, 0.07273799926042557, -0.04034300148487091, 0.9965350031852722, 0.9999179840087891, 0.007191000040620565, 0.010560999624431133, 0.9989050030708313, 0.04436499997973442, -0.014883999712765217, 0.9694769978523254, -0.17860299348831177, 0.1679760068655014, 0.9924619793891907, -0.10775599628686905, 0.058378998190164566, 0.8567489981651306, -0.28829601407051086, 0.4276289939880371, 0.9473999738693237, -0.16112199425697327, 0.27653801441192627, 0.5627779960632324, -0.19747799634933472, 0.8026729822158813, 0.6577669978141785, -0.11438000202178955, 0.7444859743118286, 0.07901199907064438, 0.0013689999468624592, 0.9968730211257935, 0.07274100184440613, -0.02020600065588951, 0.9971460103988647, 0.9888780117034912, 0.025808999314904213, -0.14647500216960907, 0.9453979730606079, -0.006663000211119652, 0.3258500099182129, 0.6921399831771851, -0.04972299933433533, 0.7200480103492737, 0.3957499861717224, -0.08134900033473969, 0.9147480130195618, 0.13914500176906586, -0.00007899999764049426, 0.9902719855308533, 0.9924669861793518, -0.10311000049114227, -0.06616800278425217, 0.9926300048828125, 0.07926999777555466, 0.09165900200605392, 0.900858998298645, 0.2553130090236664, 0.3510949909687042, 0.6513699889183044, 0.18318000435829163, 0.7363160252571106, 0.15978699922561646, -0.02714099921286106, 0.9867780208587646, 0.9955620169639587, -0.09379199892282486, 0.0077309999614953995, 0.9991030097007751, 0.01610800065100193, 0.039149001240730286, 0.9764699935913086, 0.12068899720907211, 0.17871999740600586, 0.7859060168266296, 0.10103499889373779, 0.6100350022315979, 0.16579000651836395, -0.014832000248134136, 0.986050009727478, 0.1655299961566925, -0.10078699886798859, 0.9810410141944885, -0.0046790000051259995, -0.1750659942626953, 0.9845460057258606, -0.3859579861164093, -0.06494200229644775, 0.9202280044555664, -0.3219670057296753, -0.05600599944591522, 0.9450929760932922, -0.6471610069274902, -0.11495299637317657, 0.7536370158195496, -0.5616440176963806, -0.078855000436306, 0.8236119747161865, -0.8379700183868408, -0.23749999701976776, 0.4913240075111389, -0.7512590289115906, -0.1447169929742813, 0.6439470052719116, -0.9052090048789978, -0.2807050049304962, 0.3190630078315735, -0.8249419927597046, -0.2209009975194931, 0.5202630162239075, -0.13363699615001678, 0.0291920006275177, 0.9905999898910522, -0.4039649963378906, 0.0019519999623298645, 0.9147719740867615, -0.7191359996795654, 0.002443999983370304, 0.6948649883270264, -0.9637579917907715, 0.026884999126195908, 0.26541900634765625, -0.9637719988822937, 0.2207069993019104, -0.14977200329303741, 0.03522900119423866, 0.06716900318861008, 0.9971190094947815, -0.3620629906654358, -0.01676199957728386, 0.9320030212402344, -0.6534259915351868, 0.007120999973267317, 0.7569569945335388, -0.8528590202331543, 0.11686599999666214, 0.5088940262794495, -0.8814889788627625, 0.3579840064048767, 0.3079349994659424, 0.0726580023765564, 0.02018200047314167, 0.9971529841423035, -0.37608298659324646, -0.025955000892281532, 0.926222026348114, -0.6568350195884705, -0.015021000057458878, 0.7538840174674988, -0.8238760232925415, 0.03257700055837631, 0.5658339858055115, -0.8688690066337585, 0.13078700006008148, 0.4774540066719055, 0.07265599817037582, -0.07700400054454803, 0.994379997253418, -0.0343950018286705, -0.151870995759964, 0.9878020286560059, -0.40362000465393066, -0.05282000079751015, 0.9134010076522827, -0.37586501240730286, -0.041078001260757446, 0.9257640242576599, -0.6878190040588379, -0.0810059979557991, 0.721347987651825, -0.6558970212936401, -0.052101001143455505, 0.7530509829521179, -0.8708800077438354, -0.2062380015850067, 0.44613200426101685, -0.8175939917564392, -0.12448199838399887, 0.5621780157089233, -0.8926960229873657, -0.3055669963359833, 0.33124300837516785, -0.8565059900283813, -0.21024300158023834, 0.4713769853115082, -0.15155400335788727, -0.025412000715732574, 0.9881219863891602, -0.41033700108528137, -0.0026420000940561295, 0.9119300246238708, -0.7240620255470276, 0.00047400000039488077, 0.6897349953651428, -0.9655590057373047, -0.017078999429941177, 0.2596229910850525, -0.973825991153717, -0.19711799919605255, -0.1131730005145073, 0.06214199960231781, 0.08235500007867813, 0.9946640133857727, -0.3334290087223053, 0.007625999860465527, 0.9427440166473389, -0.608610987663269, 0.04885999858379364, 0.7919629812240601, -0.8253309726715088, 0.14631199836730957, 0.5453640222549438, -0.9105669856071472, 0.3146660029888153, 0.2680560052394867, 0.16523399949073792, 0.04589800164103508, 0.985185980796814, -0.32260099053382874, -0.010471000336110592, 0.9464769959449768, -0.5639140009880066, 0.015166000463068485, 0.8256940245628357, -0.758965015411377, 0.05617399886250496, 0.6487039923667908, -0.8382350206375122, 0.14245299994945526, 0.5263739824295044, 0.9727830290794373, -0.019794000312685966, 0.2308720052242279, 0.9828159809112549, -0.036465998739004135, 0.18095199763774872, 0.9050639867782593, -0.02252200059592724, 0.4246790111064911, 0.8354039788246155, -0.03220000118017197, 0.5486930012702942, 0.6465700268745422, -0.045921001583337784, 0.7614709734916687, 0.4826749861240387, -0.04895399883389473, 0.8744300007820129, 0.4453999996185303, 0.17256900668144226, 0.8785430192947388, 0.47231200337409973, 0.13768500089645386, 0.8706120252609253, 0.4824250042438507, 0.3898639976978302, 0.7843930125236511, 0.641398012638092, 0.4275979995727539, 0.6370000243186951, 0.9863939881324768, 0.0994419977068901, 0.13091400265693665, 0.9154840111732483, 0.2120320051908493, 0.34195101261138916, 0.7246469855308533, 0.245046004652977, 0.6440799832344055, 0.35685500502586365, 0.17885500192642212, 0.9168779850006104, 0.14101800322532654, 0.24263200163841248, 0.9598140120506287, 0.9366779923439026, 0.16484400629997253, 0.3089669942855835, 0.7982620000839233, 0.2441370040178299, 0.5506129860877991, 0.4769439995288849, 0.2904820144176483, 0.8295450210571289, 0.4125959873199463, -0.017246000468730927, 0.9107509851455688, 0.341374009847641, -0.37689098715782166, 0.8610560297966003, 0.9026100039482117, 0.14119599759578705, 0.40664398670196533, 0.7326020002365112, 0.1491979956626892, 0.6641039848327637, 0.38115599751472473, 0.15792299807071686, 0.9109230041503906, 0.5317370295524597, -0.02143399976193905, 0.846638023853302, 0.7915729880332947, -0.3539769947528839, 0.49810999631881714, 0.9087340235710144, -0.07959599792957306, 0.40971601009368896, 0.9386569857597351, -0.17680299282073975, 0.29607900977134705, 0.7781569957733154, -0.19467000663280487, 0.5971400141716003, 0.738847017288208, -0.07674700021743774, 0.6694890260696411, 0.43915998935699463, -0.22276799380779266, 0.870352029800415, 0.3863860070705414, -0.0790880024433136, 0.918940007686615, 0.3576120138168335, 0.07325199991464615, 0.9309930205345154, 0.5227140188217163, 0.16167999804019928, 0.8370360136032104, 0.4246380031108856, 0.3233239948749542, 0.845661997795105, 0.733618974685669, 0.48907899856567383, 0.4718089997768402, 0.9886019825935364, -0.10717800259590149, 0.10573200136423111, 0.9131960272789001, -0.22303399443626404, 0.3410690128803253, 0.7163559794425964, -0.25636500120162964, 0.6489310264587402, 0.35149699449539185, -0.15968100726604462, 0.922469973564148, 0.07999800145626068, -0.2107039988040924, 0.9742709994316101, 0.9883249998092651, 0.04732999950647354, 0.14482200145721436, 0.9210500121116638, 0.07413999736309052, 0.3823229968547821, 0.6804890036582947, 0.11895299702882767, 0.7230389714241028, 0.4935390055179596, -0.10269299894571304, 0.8636389970779419, 0.3912479877471924, -0.4752289950847626, 0.7880880236625671, 0.9700270295143127, 0.07970499992370605, 0.2295520007610321, 0.831250011920929, 0.10592100024223328, 0.5457149744033813, 0.4774230122566223, 0.13252699375152588, 0.8686220049858093, 0.47922399640083313, -0.0024129999801516533, 0.877689003944397, 0.6902980208396912, -0.29711300134658813, 0.6597059965133667, 0.6312130093574524, 0.4550989866256714, 0.628055989742279, 0.26494699716567993, 0.5426689982414246, 0.797065019607544, 0.4216960072517395, 0.6728450059890747, 0.6078259944915771, 0.7324270009994507, 0.5829970240592957, 0.35166099667549133, 0.5086709856987, 0.8606399893760681, -0.023507000878453255, 0.7640720009803772, 0.6449369788169861, -0.015798000618815422, 0.19029100239276886, 0.2773289978504181, -0.9417420029640198, 0.02588699944317341, 0.0005750000127591193, -0.9996650218963623, -0.33045700192451477, -0.4387669861316681, -0.8356329798698425, -0.6271269917488098, -0.4645389914512634, -0.6252319812774658, -0.02311599999666214, 0.2469020038843155, 0.9687650203704834, -0.012950999662280083, 0.45514100790023804, 0.8903250098228455, -0.001180000021122396, 0.9888520240783691, 0.14889399707317352, 0.019520999863743782, 0.6841210126876831, -0.7291070222854614, 0.012253000400960445, 0.007784999907016754, -0.9998949766159058, 0.3314639925956726, -0.3832260072231293, 0.8621309995651245, 0.08274699747562408, -0.16047699749469757, 0.9835649728775024, -0.381630003452301, 0.638043999671936, 0.6687729954719543, -0.44988399744033813, 0.787883996963501, -0.42052799463272095, -0.2780170142650604, 0.5983560085296631, -0.7514500021934509, 0.7378140091896057, -0.4918749928474426, 0.4622659981250763, 0.6153389811515808, -0.45540300011634827, 0.6434019804000854, -0.43678900599479675, 0.33411499857902527, 0.8352140188217163, -0.7429530024528503, 0.6388909816741943, -0.1995989978313446, -0.7432950139045715, 0.5977380275726318, -0.3003700077533722, 0.7197920083999634, 0.5168960094451904, 0.4633769989013672, 0.22183099389076233, 0.4485720098018646, 0.8657789826393127, 0.08203200250864029, 0.15848000347614288, 0.9839479923248291, 0.5953459739685059, 0.4811680018901825, 0.6434599757194519, -0.37556400895118713, -0.6215270161628723, 0.6875, -0.42666301131248474, -0.33534398674964905, 0.8399419784545898, -0.44476398825645447, -0.7887529730796814, -0.42432600259780884, -0.7557309865951538, -0.6222699880599976, -0.20408600568771362, -0.4292669892311096, -0.5361850261688232, -0.7267979979515076, -0.7655900120735168, -0.5671039819717407, -0.30375200510025024, 0.007348000071942806, -0.21113499999046326, 0.9774289727210999, -0.01990099996328354, -0.4373210072517395, 0.8990849852561951, -0.008775000460445881, -0.9864199757575989, 0.16400499641895294, 0.024855999276041985, -0.6829339861869812, -0.7300570011138916, 0.014514000155031681, 0.03655200079083443, -0.9992259740829468, 0.40192899107933044, -0.4676550030708313, 0.7872430086135864, 0.41696101427078247, -0.6813820004463196, 0.6015490293502808, 0.5033609867095947, -0.8637740015983582, -0.022839000448584557, 0.2058819979429245, -0.2945750057697296, -0.9331870079040527, -0.2351589947938919, 0.5535579919815063, -0.7989199757575989, 0.6533820033073425, -0.435588002204895, 0.619156002998352, 0.7555500268936157, -0.554410994052887, 0.3489600121974945, 0.7765160202980042, -0.6298360228538513, -0.01814199984073639, 0.025975000113248825, 0.008264999836683273, -0.9996280074119568, -0.6086519956588745, 0.49536699056625366, -0.6198019981384277, -0.9813200235366821, 0.19238099455833435, 0, -0.831650972366333, 0.5552989840507507, 0, -0.4425640106201172, 0.38308998942375183, 0.8107889890670776, -0.5623509883880615, 0.11026400327682495, 0.8195139765739441, 0.367917001247406, -0.15178599953651428, 0.917385995388031, 0.3960669934749603, -0.07774800062179565, 0.9149240255355835, 0.3283520042896271, -0.13562799990177155, 0.9347670078277588, 0.3530749976634979, -0.06952299922704697, 0.9330080151557922, -0.5935590267181396, 0.20035800337791443, 0.7794510126113892, -0.6201800107955933, 0.12245599925518036, 0.7748429775238037, -0.5552989840507507, 0.831650972366333, 0, -0.2616960108280182, 0.523730993270874, 0.8106920123100281, 0.2819640040397644, -0.2819199860095978, 0.9170699715614319, 0.25169798731803894, -0.25161200761795044, 0.9345269799232483, -0.4710330069065094, 0.41249701380729675, 0.7797269821166992, -0.19238099455833435, 0.9813200235366821, 0, -0.04031100124120712, 0.5840420126914978, 0.8107219934463501, 0.1517850011587143, -0.3678950071334839, 0.9173960089683533, 0.13561999797821045, -0.3282899856567383, 0.9347900152206421, -0.27737799286842346, 0.561601996421814, 0.779528021812439, -1, 0, 0, 0.29074999690055847, 0.5413560271263123, 0.7889220118522644, 0.07767199724912643, -0.39607399702072144, 0.9149270057678223, 0.0693729966878891, -0.3530940115451813, 0.9330130219459534, -0.12221000343561172, 0.6202139854431152, 0.7748550176620483, 0.19238099455833435, 0.9813200235366821, 0, 0.5552989840507507, 0.831650972366333, 0, 0.38308998942375183, 0.4425640106201172, 0.8107889890670776, 0.11026400327682495, 0.5623509883880615, 0.8195139765739441, -0.15178599953651428, -0.367917001247406, 0.917385995388031, -0.07774800062179565, -0.3960669934749603, 0.9149240255355835, -0.13562799990177155, -0.3283520042896271, 0.9347670078277588, -0.06952299922704697, -0.3530749976634979, 0.9330080151557922, 0.20035800337791443, 0.5935590267181396, 0.7794510126113892, 0.12245599925518036, 0.6201800107955933, 0.7748429775238037, 0.831650972366333, 0.5552989840507507, 0, 0.523730993270874, 0.2616960108280182, 0.8106920123100281, -0.2819199860095978, -0.2819640040397644, 0.9170699715614319, -0.25161200761795044, -0.25169798731803894, 0.9345269799232483, 0.41249701380729675, 0.4710330069065094, 0.7797269821166992, 0.9813200235366821, 0.19238099455833435, 0, 0.5840420126914978, 0.04031100124120712, 0.8107219934463501, -0.3678950071334839, -0.1517850011587143, 0.9173960089683533, -0.3282899856567383, -0.13561999797821045, 0.9347900152206421, 0.561601996421814, 0.27737799286842346, 0.779528021812439, -1, 0, 0, 0.5413560271263123, -0.29074999690055847, 0.7889220118522644, -0.39607399702072144, -0.07767199724912643, 0.9149270057678223, -0.3530940115451813, -0.0693729966878891, 0.9330130219459534, 0.6202139854431152, 0.12221000343561172, 0.7748550176620483, 0.9813200235366821, -0.19238099455833435, 0, 0.831650972366333, -0.5552989840507507, 0, 0.4425640106201172, -0.38308998942375183, 0.8107889890670776, 0.5623509883880615, -0.11026400327682495, 0.8195139765739441, -0.367917001247406, 0.15178599953651428, 0.917385995388031, -0.3960669934749603, 0.07774800062179565, 0.9149240255355835, -0.3283520042896271, 0.13562799990177155, 0.9347670078277588, -0.3530749976634979, 0.06952299922704697, 0.9330080151557922, 0.5935590267181396, -0.20035800337791443, 0.7794510126113892, 0.6201800107955933, -0.12245599925518036, 0.7748429775238037, 0.5552989840507507, -0.831650972366333, 0, 0.2616960108280182, -0.523730993270874, 0.8106920123100281, -0.2819640040397644, 0.2819199860095978, 0.9170699715614319, -0.25169798731803894, 0.25161200761795044, 0.9345269799232483, 0.4710330069065094, -0.41249701380729675, 0.7797269821166992, 0.19238099455833435, -0.9813200235366821, 0, 0.04031100124120712, -0.5840420126914978, 0.8107219934463501, -0.1517850011587143, 0.3678950071334839, 0.9173960089683533, -0.13561999797821045, 0.3282899856567383, 0.9347900152206421, 0.27737799286842346, -0.561601996421814, 0.779528021812439, -1, 0, 0, -0.29074999690055847, -0.5413560271263123, 0.7889220118522644, -0.07767199724912643, 0.39607399702072144, 0.9149270057678223, -0.0693729966878891, 0.3530940115451813, 0.9330130219459534, 0.12221000343561172, -0.6202139854431152, 0.7748550176620483, -0.19238099455833435, -0.9813200235366821, 0, -0.5552989840507507, -0.831650972366333, 0, -0.38308998942375183, -0.4425640106201172, 0.8107889890670776, -0.11026400327682495, -0.5623509883880615, 0.8195139765739441, 0.15178599953651428, 0.367917001247406, 0.917385995388031, 0.07774800062179565, 0.3960669934749603, 0.9149240255355835, 0.13562799990177155, 0.3283520042896271, 0.9347670078277588, 0.06952299922704697, 0.3530749976634979, 0.9330080151557922, -0.20035800337791443, -0.5935590267181396, 0.7794510126113892, -0.12245599925518036, -0.6201800107955933, 0.7748429775238037, -0.831650972366333, -0.5552989840507507, 0, -0.523730993270874, -0.2616960108280182, 0.8106920123100281, 0.2819199860095978, 0.2819640040397644, 0.9170699715614319, 0.25161200761795044, 0.25169798731803894, 0.9345269799232483, -0.41249701380729675, -0.4710330069065094, 0.7797269821166992, -0.9813200235366821, -0.19238099455833435, 0, -0.5840420126914978, -0.04031100124120712, 0.8107219934463501, 0.3678950071334839, 0.1517850011587143, 0.9173960089683533, 0.3282899856567383, 0.13561999797821045, 0.9347900152206421, -0.561601996421814, -0.27737799286842346, 0.779528021812439, -1, 0, 0, -0.5413560271263123, 0.29074999690055847, 0.7889220118522644, 0.39607399702072144, 0.07767199724912643, 0.9149270057678223, 0.3530940115451813, 0.0693729966878891, 0.9330130219459534, -0.6202139854431152, -0.12221000343561172, 0.7748550176620483, -0.6201800107955933, 0.12245599925518036, 0.7748429775238037, -0.5616469979286194, 0.27755099534988403, 0.7794349789619446, -0.8979210257530212, 0.37159600853919983, 0.23590999841690063, -0.9542099833488464, 0.18841099739074707, 0.23234599828720093, -0.9101200103759766, 0.3766449987888336, 0.17268399894237518, -0.966795027256012, 0.19089600443840027, 0.16990099847316742, -0.8549879789352417, 0.35383298993110657, 0.3792079985141754, -0.9100499749183655, 0.17969100177288055, 0.3735229969024658, -0.8064150214195251, 0.2724289894104004, 0.5248600244522095, -0.8383409976959229, 0.16553199291229248, 0.5194069743156433, -0.4125959873199463, 0.47094500064849854, 0.7797279953956604, -0.687192976474762, 0.687192976474762, 0.23565199971199036, -0.6965190172195435, 0.6965190172195435, 0.17240400612354279, -0.6544880270957947, 0.6544870138168335, 0.37853899598121643, -0.6404970288276672, 0.5611429810523987, 0.5242909789085388, -0.20047900080680847, 0.5933949947357178, 0.7795450091362, -0.3715969920158386, 0.8979210257530212, 0.23590999841690063, -0.3766449987888336, 0.9101200103759766, 0.17268399894237518, -0.35383298993110657, 0.8549879789352417, 0.3792079985141754, -0.3770729899406433, 0.7630789875984192, 0.5249059796333313, -0.12245900183916092, 0.6201940178871155, 0.7748309969902039, -0.18841099739074707, 0.9542099833488464, 0.23234599828720093, -0.19089600443840027, 0.966795027256012, 0.16990099847316742, -0.17969200015068054, 0.9100499749183655, 0.3735229969024658, -0.16553199291229248, 0.8383409976959229, 0.5194069743156433, 0.12245599925518036, 0.6201800107955933, 0.7748429775238037, 0.27755099534988403, 0.5616469979286194, 0.7794349789619446, 0.37159600853919983, 0.8979210257530212, 0.23590999841690063, 0.18841099739074707, 0.9542099833488464, 0.23234599828720093, 0.3766449987888336, 0.9101200103759766, 0.17268399894237518, 0.19089600443840027, 0.966795027256012, 0.16990099847316742, 0.35383298993110657, 0.8549879789352417, 0.3792079985141754, 0.17969100177288055, 0.9100499749183655, 0.3735229969024658, 0.2724289894104004, 0.8064150214195251, 0.5248600244522095, 0.16553199291229248, 0.8383409976959229, 0.5194069743156433, 0.47094500064849854, 0.4125959873199463, 0.7797279953956604, 0.687192976474762, 0.687192976474762, 0.23565199971199036, 0.6965190172195435, 0.6965190172195435, 0.17240400612354279, 0.6544870138168335, 0.6544880270957947, 0.37853899598121643, 0.5611429810523987, 0.6404970288276672, 0.5242909789085388, 0.5933949947357178, 0.20047900080680847, 0.7795450091362, 0.8979210257530212, 0.3715969920158386, 0.23590999841690063, 0.9101200103759766, 0.3766449987888336, 0.17268399894237518, 0.8549879789352417, 0.35383298993110657, 0.3792079985141754, 0.7630789875984192, 0.3770729899406433, 0.5249059796333313, 0.6201940178871155, 0.12245900183916092, 0.7748309969902039, 0.9542099833488464, 0.18841099739074707, 0.23234599828720093, 0.966795027256012, 0.19089600443840027, 0.16990099847316742, 0.9100499749183655, 0.17969200015068054, 0.3735229969024658, 0.8383409976959229, 0.16553199291229248, 0.5194069743156433, 0.6201800107955933, -0.12245599925518036, 0.7748429775238037, 0.5616469979286194, -0.27755099534988403, 0.7794349789619446, 0.8979210257530212, -0.37159600853919983, 0.23590999841690063, 0.9542099833488464, -0.18841099739074707, 0.23234599828720093, 0.9101200103759766, -0.3766449987888336, 0.17268399894237518, 0.966795027256012, -0.19089600443840027, 0.16990099847316742, 0.8549879789352417, -0.35383298993110657, 0.3792079985141754, 0.9100499749183655, -0.17969100177288055, 0.3735229969024658, 0.8064150214195251, -0.2724289894104004, 0.5248600244522095, 0.8383409976959229, -0.16553199291229248, 0.5194069743156433, 0.4125959873199463, -0.47094500064849854, 0.7797279953956604, 0.687192976474762, -0.687192976474762, 0.23565199971199036, 0.6965190172195435, -0.6965190172195435, 0.17240400612354279, 0.6544880270957947, -0.6544870138168335, 0.37853899598121643, 0.6404970288276672, -0.5611429810523987, 0.5242909789085388, 0.20047900080680847, -0.5933949947357178, 0.7795450091362, 0.3715969920158386, -0.8979210257530212, 0.23590999841690063, 0.3766449987888336, -0.9101200103759766, 0.17268399894237518, 0.35383298993110657, -0.8549879789352417, 0.3792079985141754, 0.3770729899406433, -0.7630789875984192, 0.5249059796333313, 0.12245900183916092, -0.6201940178871155, 0.7748309969902039, 0.18841099739074707, -0.9542099833488464, 0.23234599828720093, 0.19089600443840027, -0.966795027256012, 0.16990099847316742, 0.17969200015068054, -0.9100499749183655, 0.3735229969024658, 0.16553199291229248, -0.8383409976959229, 0.5194069743156433, -0.12245599925518036, -0.6201800107955933, 0.7748429775238037, -0.27755099534988403, -0.5616469979286194, 0.7794349789619446, -0.37159600853919983, -0.8979210257530212, 0.23590999841690063, -0.18841099739074707, -0.9542099833488464, 0.23234599828720093, -0.3766449987888336, -0.9101200103759766, 0.17268399894237518, -0.19089600443840027, -0.966795027256012, 0.16990099847316742, -0.35383298993110657, -0.8549879789352417, 0.3792079985141754, -0.17969100177288055, -0.9100499749183655, 0.3735229969024658, -0.2724289894104004, -0.8064150214195251, 0.5248600244522095, -0.16553199291229248, -0.8383409976959229, 0.5194069743156433, -0.47094500064849854, -0.4125959873199463, 0.7797279953956604, -0.687192976474762, -0.687192976474762, 0.23565199971199036, -0.6965190172195435, -0.6965190172195435, 0.17240400612354279, -0.6544870138168335, -0.6544880270957947, 0.37853899598121643, -0.5611429810523987, -0.6404970288276672, 0.5242909789085388, -0.5933949947357178, -0.20047900080680847, 0.7795450091362, -0.8979210257530212, -0.3715969920158386, 0.23590999841690063, -0.9101200103759766, -0.3766449987888336, 0.17268399894237518, -0.8549879789352417, -0.35383298993110657, 0.3792079985141754, -0.7630789875984192, -0.3770729899406433, 0.5249059796333313, -0.6201940178871155, -0.12245900183916092, 0.7748309969902039, -0.9542099833488464, -0.18841099739074707, 0.23234599828720093, -0.966795027256012, -0.19089600443840027, 0.16990099847316742, -0.9100499749183655, -0.17969200015068054, 0.3735229969024658, -0.8383409976959229, -0.16553199291229248, 0.5194069743156433 ]); var teapotTexCoords = new Float32Array([ 2, 2, 0, 1.75, 2, 0, 1.75, 1.975000023841858, 0, 2, 1.975000023841858, 0, 1.75, 1.9500000476837158, 0, 2, 1.9500000476837158, 0, 1.75, 1.9249999523162842, 0, 2, 1.9249999523162842, 0, 1.75, 1.899999976158142, 0, 2, 1.899999976158142, 0, 1.5, 2, 0, 1.5, 1.975000023841858, 0, 1.5, 1.9500000476837158, 0, 1.5, 1.9249999523162842, 0, 1.5, 1.899999976158142, 0, 1.25, 2, 0, 1.25, 1.975000023841858, 0, 1.25, 1.9500000476837158, 0, 1.25, 1.9249999523162842, 0, 1.25, 1.899999976158142, 0, 1, 2, 0, 1, 1.975000023841858, 0, 1, 1.9500000476837158, 0, 1, 1.9249999523162842, 0, 1, 1.899999976158142, 0, 1, 2, 0, 0.75, 2, 0, 0.75, 1.975000023841858, 0, 1, 1.975000023841858, 0, 0.75, 1.9500000476837158, 0, 1, 1.9500000476837158, 0, 0.75, 1.9249999523162842, 0, 1, 1.9249999523162842, 0, 0.75, 1.899999976158142, 0, 1, 1.899999976158142, 0, 0.5, 2, 0, 0.5, 1.975000023841858, 0, 0.5, 1.9500000476837158, 0, 0.5, 1.9249999523162842, 0, 0.5, 1.899999976158142, 0, 0.25, 2, 0, 0.25, 1.975000023841858, 0, 0.25, 1.9500000476837158, 0, 0.25, 1.9249999523162842, 0, 0.25, 1.899999976158142, 0, 0, 2, 0, 0, 1.975000023841858, 0, 0, 1.9500000476837158, 0, 0, 1.9249999523162842, 0, 0, 1.899999976158142, 0, 2, 2, 0, 1.75, 2, 0, 1.75, 1.975000023841858, 0, 2, 1.975000023841858, 0, 1.75, 1.9500000476837158, 0, 2, 1.9500000476837158, 0, 1.75, 1.9249999523162842, 0, 2, 1.9249999523162842, 0, 1.75, 1.899999976158142, 0, 2, 1.899999976158142, 0, 1.5, 2, 0, 1.5, 1.975000023841858, 0, 1.5, 1.9500000476837158, 0, 1.5, 1.9249999523162842, 0, 1.5, 1.899999976158142, 0, 1.25, 2, 0, 1.25, 1.975000023841858, 0, 1.25, 1.9500000476837158, 0, 1.25, 1.9249999523162842, 0, 1.25, 1.899999976158142, 0, 1, 2, 0, 1, 1.975000023841858, 0, 1, 1.9500000476837158, 0, 1, 1.9249999523162842, 0, 1, 1.899999976158142, 0, 1, 2, 0, 0.75, 2, 0, 0.75, 1.975000023841858, 0, 1, 1.975000023841858, 0, 0.75, 1.9500000476837158, 0, 1, 1.9500000476837158, 0, 0.75, 1.9249999523162842, 0, 1, 1.9249999523162842, 0, 0.75, 1.899999976158142, 0, 1, 1.899999976158142, 0, 0.5, 2, 0, 0.5, 1.975000023841858, 0, 0.5, 1.9500000476837158, 0, 0.5, 1.9249999523162842, 0, 0.5, 1.899999976158142, 0, 0.25, 2, 0, 0.25, 1.975000023841858, 0, 0.25, 1.9500000476837158, 0, 0.25, 1.9249999523162842, 0, 0.25, 1.899999976158142, 0, 0, 2, 0, 0, 1.975000023841858, 0, 0, 1.9500000476837158, 0, 0, 1.9249999523162842, 0, 0, 1.899999976158142, 0, 2, 1.899999976158142, 0, 1.75, 1.899999976158142, 0, 1.75, 1.6749999523162842, 0, 2, 1.6749999523162842, 0, 1.75, 1.4500000476837158, 0, 2, 1.4500000476837158, 0, 1.75, 1.225000023841858, 0, 2, 1.225000023841858, 0, 1.75, 1, 0, 2, 1, 0, 1.5, 1.899999976158142, 0, 1.5, 1.6749999523162842, 0, 1.5, 1.4500000476837158, 0, 1.5, 1.225000023841858, 0, 1.5, 1, 0, 1.25, 1.899999976158142, 0, 1.25, 1.6749999523162842, 0, 1.25, 1.4500000476837158, 0, 1.25, 1.225000023841858, 0, 1.25, 1, 0, 1, 1.899999976158142, 0, 1, 1.6749999523162842, 0, 1, 1.4500000476837158, 0, 1, 1.225000023841858, 0, 1, 1, 0, 1, 1.899999976158142, 0, 0.75, 1.899999976158142, 0, 0.75, 1.6749999523162842, 0, 1, 1.6749999523162842, 0, 0.75, 1.4500000476837158, 0, 1, 1.4500000476837158, 0, 0.75, 1.225000023841858, 0, 1, 1.225000023841858, 0, 0.75, 1, 0, 1, 1, 0, 0.5, 1.899999976158142, 0, 0.5, 1.6749999523162842, 0, 0.5, 1.4500000476837158, 0, 0.5, 1.225000023841858, 0, 0.5, 1, 0, 0.25, 1.899999976158142, 0, 0.25, 1.6749999523162842, 0, 0.25, 1.4500000476837158, 0, 0.25, 1.225000023841858, 0, 0.25, 1, 0, 0, 1.899999976158142, 0, 0, 1.6749999523162842, 0, 0, 1.4500000476837158, 0, 0, 1.225000023841858, 0, 0, 1, 0, 2, 1.899999976158142, 0, 1.75, 1.899999976158142, 0, 1.75, 1.6749999523162842, 0, 2, 1.6749999523162842, 0, 1.75, 1.4500000476837158, 0, 2, 1.4500000476837158, 0, 1.75, 1.225000023841858, 0, 2, 1.225000023841858, 0, 1.75, 1, 0, 2, 1, 0, 1.5, 1.899999976158142, 0, 1.5, 1.6749999523162842, 0, 1.5, 1.4500000476837158, 0, 1.5, 1.225000023841858, 0, 1.5, 1, 0, 1.25, 1.899999976158142, 0, 1.25, 1.6749999523162842, 0, 1.25, 1.4500000476837158, 0, 1.25, 1.225000023841858, 0, 1.25, 1, 0, 1, 1.899999976158142, 0, 1, 1.6749999523162842, 0, 1, 1.4500000476837158, 0, 1, 1.225000023841858, 0, 1, 1, 0, 1, 1.899999976158142, 0, 0.75, 1.899999976158142, 0, 0.75, 1.6749999523162842, 0, 1, 1.6749999523162842, 0, 0.75, 1.4500000476837158, 0, 1, 1.4500000476837158, 0, 0.75, 1.225000023841858, 0, 1, 1.225000023841858, 0, 0.75, 1, 0, 1, 1, 0, 0.5, 1.899999976158142, 0, 0.5, 1.6749999523162842, 0, 0.5, 1.4500000476837158, 0, 0.5, 1.225000023841858, 0, 0.5, 1, 0, 0.25, 1.899999976158142, 0, 0.25, 1.6749999523162842, 0, 0.25, 1.4500000476837158, 0, 0.25, 1.225000023841858, 0, 0.25, 1, 0, 0, 1.899999976158142, 0, 0, 1.6749999523162842, 0, 0, 1.4500000476837158, 0, 0, 1.225000023841858, 0, 0, 1, 0, 2, 1, 0, 1.75, 1, 0, 1.75, 0.8500000238418579, 0, 2, 0.8500000238418579, 0, 1.75, 0.699999988079071, 0, 2, 0.699999988079071, 0, 1.75, 0.550000011920929, 0, 2, 0.550000011920929, 0, 1.75, 0.4000000059604645, 0, 2, 0.4000000059604645, 0, 1.5, 1, 0, 1.5, 0.8500000238418579, 0, 1.5, 0.699999988079071, 0, 1.5, 0.550000011920929, 0, 1.5, 0.4000000059604645, 0, 1.25, 1, 0, 1.25, 0.8500000238418579, 0, 1.25, 0.699999988079071, 0, 1.25, 0.550000011920929, 0, 1.25, 0.4000000059604645, 0, 1, 1, 0, 1, 0.8500000238418579, 0, 1, 0.699999988079071, 0, 1, 0.550000011920929, 0, 1, 0.4000000059604645, 0, 1, 1, 0, 0.75, 1, 0, 0.75, 0.8500000238418579, 0, 1, 0.8500000238418579, 0, 0.75, 0.699999988079071, 0, 1, 0.699999988079071, 0, 0.75, 0.550000011920929, 0, 1, 0.550000011920929, 0, 0.75, 0.4000000059604645, 0, 1, 0.4000000059604645, 0, 0.5, 1, 0, 0.5, 0.8500000238418579, 0, 0.5, 0.699999988079071, 0, 0.5, 0.550000011920929, 0, 0.5, 0.4000000059604645, 0, 0.25, 1, 0, 0.25, 0.8500000238418579, 0, 0.25, 0.699999988079071, 0, 0.25, 0.550000011920929, 0, 0.25, 0.4000000059604645, 0, 0, 1, 0, 0, 0.8500000238418579, 0, 0, 0.699999988079071, 0, 0, 0.550000011920929, 0, 0, 0.4000000059604645, 0, 2, 1, 0, 1.75, 1, 0, 1.75, 0.8500000238418579, 0, 2, 0.8500000238418579, 0, 1.75, 0.699999988079071, 0, 2, 0.699999988079071, 0, 1.75, 0.550000011920929, 0, 2, 0.550000011920929, 0, 1.75, 0.4000000059604645, 0, 2, 0.4000000059604645, 0, 1.5, 1, 0, 1.5, 0.8500000238418579, 0, 1.5, 0.699999988079071, 0, 1.5, 0.550000011920929, 0, 1.5, 0.4000000059604645, 0, 1.25, 1, 0, 1.25, 0.8500000238418579, 0, 1.25, 0.699999988079071, 0, 1.25, 0.550000011920929, 0, 1.25, 0.4000000059604645, 0, 1, 1, 0, 1, 0.8500000238418579, 0, 1, 0.699999988079071, 0, 1, 0.550000011920929, 0, 1, 0.4000000059604645, 0, 1, 1, 0, 0.75, 1, 0, 0.75, 0.8500000238418579, 0, 1, 0.8500000238418579, 0, 0.75, 0.699999988079071, 0, 1, 0.699999988079071, 0, 0.75, 0.550000011920929, 0, 1, 0.550000011920929, 0, 0.75, 0.4000000059604645, 0, 1, 0.4000000059604645, 0, 0.5, 1, 0, 0.5, 0.8500000238418579, 0, 0.5, 0.699999988079071, 0, 0.5, 0.550000011920929, 0, 0.5, 0.4000000059604645, 0, 0.25, 1, 0, 0.25, 0.8500000238418579, 0, 0.25, 0.699999988079071, 0, 0.25, 0.550000011920929, 0, 0.25, 0.4000000059604645, 0, 0, 1, 0, 0, 0.8500000238418579, 0, 0, 0.699999988079071, 0, 0, 0.550000011920929, 0, 0, 0.4000000059604645, 0, 2, 0.4000000059604645, 0, 1.75, 0.4000000059604645, 0, 1.75, 0.30000001192092896, 0, 2, 0.30000001192092896, 0, 1.75, 0.20000000298023224, 0, 2, 0.20000000298023224, 0, 1.75, 0.10000000149011612, 0, 2, 0.10000000149011612, 0, 1.75, 0, 0, 2, 0, 0, 1.5, 0.4000000059604645, 0, 1.5, 0.30000001192092896, 0, 1.5, 0.20000000298023224, 0, 1.5, 0.10000000149011612, 0, 1.5, 0, 0, 1.25, 0.4000000059604645, 0, 1.25, 0.30000001192092896, 0, 1.25, 0.20000000298023224, 0, 1.25, 0.10000000149011612, 0, 1.25, 0, 0, 1, 0.4000000059604645, 0, 1, 0.30000001192092896, 0, 1, 0.20000000298023224, 0, 1, 0.10000000149011612, 0, 1, 0, 0, 1, 0.4000000059604645, 0, 0.75, 0.4000000059604645, 0, 0.75, 0.30000001192092896, 0, 1, 0.30000001192092896, 0, 0.75, 0.20000000298023224, 0, 1, 0.20000000298023224, 0, 0.75, 0.10000000149011612, 0, 1, 0.10000000149011612, 0, 0.75, 0, 0, 1, 0, 0, 0.5, 0.4000000059604645, 0, 0.5, 0.30000001192092896, 0, 0.5, 0.20000000298023224, 0, 0.5, 0.10000000149011612, 0, 0.5, 0, 0, 0.25, 0.4000000059604645, 0, 0.25, 0.30000001192092896, 0, 0.25, 0.20000000298023224, 0, 0.25, 0.10000000149011612, 0, 0.25, 0, 0, 0, 0.4000000059604645, 0, 0, 0.30000001192092896, 0, 0, 0.20000000298023224, 0, 0, 0.10000000149011612, 0, 0, 0, 0, 2, 0.4000000059604645, 0, 1.75, 0.4000000059604645, 0, 1.75, 0.30000001192092896, 0, 2, 0.30000001192092896, 0, 1.75, 0.20000000298023224, 0, 2, 0.20000000298023224, 0, 1.75, 0.10000000149011612, 0, 2, 0.10000000149011612, 0, 1.75, 0, 0, 2, 0, 0, 1.5, 0.4000000059604645, 0, 1.5, 0.30000001192092896, 0, 1.5, 0.20000000298023224, 0, 1.5, 0.10000000149011612, 0, 1.5, 0, 0, 1.25, 0.4000000059604645, 0, 1.25, 0.30000001192092896, 0, 1.25, 0.20000000298023224, 0, 1.25, 0.10000000149011612, 0, 1.25, 0, 0, 1, 0.4000000059604645, 0, 1, 0.30000001192092896, 0, 1, 0.20000000298023224, 0, 1, 0.10000000149011612, 0, 1, 0, 0, 1, 0.4000000059604645, 0, 0.75, 0.4000000059604645, 0, 0.75, 0.30000001192092896, 0, 1, 0.30000001192092896, 0, 0.75, 0.20000000298023224, 0, 1, 0.20000000298023224, 0, 0.75, 0.10000000149011612, 0, 1, 0.10000000149011612, 0, 0.75, 0, 0, 1, 0, 0, 0.5, 0.4000000059604645, 0, 0.5, 0.30000001192092896, 0, 0.5, 0.20000000298023224, 0, 0.5, 0.10000000149011612, 0, 0.5, 0, 0, 0.25, 0.4000000059604645, 0, 0.25, 0.30000001192092896, 0, 0.25, 0.20000000298023224, 0, 0.25, 0.10000000149011612, 0, 0.25, 0, 0, 0, 0.4000000059604645, 0, 0, 0.30000001192092896, 0, 0, 0.20000000298023224, 0, 0, 0.10000000149011612, 0, 0, 0, 0, 1, 1, 0, 0.875, 1, 0, 0.875, 0.875, 0, 1, 0.875, 0, 0.875, 0.75, 0, 1, 0.75, 0, 0.875, 0.625, 0, 1, 0.625, 0, 0.875, 0.5, 0, 1, 0.5, 0, 0.75, 1, 0, 0.75, 0.875, 0, 0.75, 0.75, 0, 0.75, 0.625, 0, 0.75, 0.5, 0, 0.625, 1, 0, 0.625, 0.875, 0, 0.625, 0.75, 0, 0.625, 0.625, 0, 0.625, 0.5, 0, 0.5, 1, 0, 0.5, 0.875, 0, 0.5, 0.75, 0, 0.5, 0.625, 0, 0.5, 0.5, 0, 0.5, 1, 0, 0.375, 1, 0, 0.375, 0.875, 0, 0.5, 0.875, 0, 0.375, 0.75, 0, 0.5, 0.75, 0, 0.375, 0.625, 0, 0.5, 0.625, 0, 0.375, 0.5, 0, 0.5, 0.5, 0, 0.25, 1, 0, 0.25, 0.875, 0, 0.25, 0.75, 0, 0.25, 0.625, 0, 0.25, 0.5, 0, 0.125, 1, 0, 0.125, 0.875, 0, 0.125, 0.75, 0, 0.125, 0.625, 0, 0.125, 0.5, 0, 0, 1, 0, 0, 0.875, 0, 0, 0.75, 0, 0, 0.625, 0, 0, 0.5, 0, 1, 0.5, 0, 0.875, 0.5, 0, 0.875, 0.375, 0, 1, 0.375, 0, 0.875, 0.25, 0, 1, 0.25, 0, 0.875, 0.125, 0, 1, 0.125, 0, 0.875, 0, 0, 1, 0, 0, 0.75, 0.5, 0, 0.75, 0.375, 0, 0.75, 0.25, 0, 0.75, 0.125, 0, 0.75, 0, 0, 0.625, 0.5, 0, 0.625, 0.375, 0, 0.625, 0.25, 0, 0.625, 0.125, 0, 0.625, 0, 0, 0.5, 0.5, 0, 0.5, 0.375, 0, 0.5, 0.25, 0, 0.5, 0.125, 0, 0.5, 0, 0, 0.5, 0.5, 0, 0.375, 0.5, 0, 0.375, 0.375, 0, 0.5, 0.375, 0, 0.375, 0.25, 0, 0.5, 0.25, 0, 0.375, 0.125, 0, 0.5, 0.125, 0, 0.375, 0, 0, 0.5, 0, 0, 0.25, 0.5, 0, 0.25, 0.375, 0, 0.25, 0.25, 0, 0.25, 0.125, 0, 0.25, 0, 0, 0.125, 0.5, 0, 0.125, 0.375, 0, 0.125, 0.25, 0, 0.125, 0.125, 0, 0.125, 0, 0, 0, 0.5, 0, 0, 0.375, 0, 0, 0.25, 0, 0, 0.125, 0, 0, 0, 0, 0.5, 0, 0, 0.625, 0, 0, 0.625, 0.22499999403953552, 0, 0.5, 0.22499999403953552, 0, 0.625, 0.44999998807907104, 0, 0.5, 0.44999998807907104, 0, 0.625, 0.675000011920929, 0, 0.5, 0.675000011920929, 0, 0.625, 0.8999999761581421, 0, 0.5, 0.8999999761581421, 0, 0.75, 0, 0, 0.75, 0.22499999403953552, 0, 0.75, 0.44999998807907104, 0, 0.75, 0.675000011920929, 0, 0.75, 0.8999999761581421, 0, 0.875, 0, 0, 0.875, 0.22499999403953552, 0, 0.875, 0.44999998807907104, 0, 0.875, 0.675000011920929, 0, 0.875, 0.8999999761581421, 0, 1, 0, 0, 1, 0.22499999403953552, 0, 1, 0.44999998807907104, 0, 1, 0.675000011920929, 0, 1, 0.8999999761581421, 0, 0, 0, 0, 0.125, 0, 0, 0.125, 0.22499999403953552, 0, 0, 0.22499999403953552, 0, 0.125, 0.44999998807907104, 0, 0, 0.44999998807907104, 0, 0.125, 0.675000011920929, 0, 0, 0.675000011920929, 0, 0.125, 0.8999999761581421, 0, 0, 0.8999999761581421, 0, 0.25, 0, 0, 0.25, 0.22499999403953552, 0, 0.25, 0.44999998807907104, 0, 0.25, 0.675000011920929, 0, 0.25, 0.8999999761581421, 0, 0.375, 0, 0, 0.375, 0.22499999403953552, 0, 0.375, 0.44999998807907104, 0, 0.375, 0.675000011920929, 0, 0.375, 0.8999999761581421, 0, 0.5, 0, 0, 0.5, 0.22499999403953552, 0, 0.5, 0.44999998807907104, 0, 0.5, 0.675000011920929, 0, 0.5, 0.8999999761581421, 0, 0.5, 0.8999999761581421, 0, 0.625, 0.8999999761581421, 0, 0.625, 0.925000011920929, 0, 0.5, 0.925000011920929, 0, 0.625, 0.949999988079071, 0, 0.5, 0.949999988079071, 0, 0.625, 0.9750000238418579, 0, 0.5, 0.9750000238418579, 0, 0.625, 1, 0, 0.5, 1, 0, 0.75, 0.8999999761581421, 0, 0.75, 0.925000011920929, 0, 0.75, 0.949999988079071, 0, 0.75, 0.9750000238418579, 0, 0.75, 1, 0, 0.875, 0.8999999761581421, 0, 0.875, 0.925000011920929, 0, 0.875, 0.949999988079071, 0, 0.875, 0.9750000238418579, 0, 0.875, 1, 0, 1, 0.8999999761581421, 0, 1, 0.925000011920929, 0, 1, 0.949999988079071, 0, 1, 0.9750000238418579, 0, 1, 1, 0, 0, 0.8999999761581421, 0, 0.125, 0.8999999761581421, 0, 0.125, 0.925000011920929, 0, 0, 0.925000011920929, 0, 0.125, 0.949999988079071, 0, 0, 0.949999988079071, 0, 0.125, 0.9750000238418579, 0, 0, 0.9750000238418579, 0, 0.125, 1, 0, 0, 1, 0, 0.25, 0.8999999761581421, 0, 0.25, 0.925000011920929, 0, 0.25, 0.949999988079071, 0, 0.25, 0.9750000238418579, 0, 0.25, 1, 0, 0.375, 0.8999999761581421, 0, 0.375, 0.925000011920929, 0, 0.375, 0.949999988079071, 0, 0.375, 0.9750000238418579, 0, 0.375, 1, 0, 0.5, 0.8999999761581421, 0, 0.5, 0.925000011920929, 0, 0.5, 0.949999988079071, 0, 0.5, 0.9750000238418579, 0, 0.5, 1, 0, 1, 1, 0, 0.875, 1, 0, 0.875, 0.75, 0, 1, 0.75, 0, 0.875, 0.5, 0, 1, 0.5, 0, 0.875, 0.25, 0, 1, 0.25, 0, 0.875, 0, 0, 1, 0, 0, 0.75, 1, 0, 0.75, 0.75, 0, 0.75, 0.5, 0, 0.75, 0.25, 0, 0.75, 0, 0, 0.625, 1, 0, 0.625, 0.75, 0, 0.625, 0.5, 0, 0.625, 0.25, 0, 0.625, 0, 0, 0.5, 1, 0, 0.5, 0.75, 0, 0.5, 0.5, 0, 0.5, 0.25, 0, 0.5, 0, 0, 0.5, 1, 0, 0.375, 1, 0, 0.375, 0.75, 0, 0.5, 0.75, 0, 0.375, 0.5, 0, 0.5, 0.5, 0, 0.375, 0.25, 0, 0.5, 0.25, 0, 0.375, 0, 0, 0.5, 0, 0, 0.25, 1, 0, 0.25, 0.75, 0, 0.25, 0.5, 0, 0.25, 0.25, 0, 0.25, 0, 0, 0.125, 1, 0, 0.125, 0.75, 0, 0.125, 0.5, 0, 0.125, 0.25, 0, 0.125, 0, 0, 0, 1, 0, 0, 0.75, 0, 0, 0.5, 0, 0, 0.25, 0, 0, 0, 0, 1, 1, 0, 0.875, 1, 0, 0.875, 0.75, 0, 1, 0.75, 0, 0.875, 0.5, 0, 1, 0.5, 0, 0.875, 0.25, 0, 1, 0.25, 0, 0.875, 0, 0, 1, 0, 0, 0.75, 1, 0, 0.75, 0.75, 0, 0.75, 0.5, 0, 0.75, 0.25, 0, 0.75, 0, 0, 0.625, 1, 0, 0.625, 0.75, 0, 0.625, 0.5, 0, 0.625, 0.25, 0, 0.625, 0, 0, 0.5, 1, 0, 0.5, 0.75, 0, 0.5, 0.5, 0, 0.5, 0.25, 0, 0.5, 0, 0, 0.5, 1, 0, 0.375, 1, 0, 0.375, 0.75, 0, 0.5, 0.75, 0, 0.375, 0.5, 0, 0.5, 0.5, 0, 0.375, 0.25, 0, 0.5, 0.25, 0, 0.375, 0, 0, 0.5, 0, 0, 0.25, 1, 0, 0.25, 0.75, 0, 0.25, 0.5, 0, 0.25, 0.25, 0, 0.25, 0, 0, 0.125, 1, 0, 0.125, 0.75, 0, 0.125, 0.5, 0, 0.125, 0.25, 0, 0.125, 0, 0, 0, 1, 0, 0, 0.75, 0, 0, 0.5, 0, 0, 0.25, 0, 0, 0, 0, 1, 1, 0, 0.875, 1, 0, 0.875, 0.75, 0, 1, 0.75, 0, 0.875, 0.5, 0, 1, 0.5, 0, 0.875, 0.25, 0, 1, 0.25, 0, 0.875, 0, 0, 1, 0, 0, 0.75, 1, 0, 0.75, 0.75, 0, 0.75, 0.5, 0, 0.75, 0.25, 0, 0.75, 0, 0, 0.625, 1, 0, 0.625, 0.75, 0, 0.625, 0.5, 0, 0.625, 0.25, 0, 0.625, 0, 0, 0.5, 1, 0, 0.5, 0.75, 0, 0.5, 0.5, 0, 0.5, 0.25, 0, 0.5, 0, 0, 0.5, 1, 0, 0.375, 1, 0, 0.375, 0.75, 0, 0.5, 0.75, 0, 0.375, 0.5, 0, 0.5, 0.5, 0, 0.375, 0.25, 0, 0.5, 0.25, 0, 0.375, 0, 0, 0.5, 0, 0, 0.25, 1, 0, 0.25, 0.75, 0, 0.25, 0.5, 0, 0.25, 0.25, 0, 0.25, 0, 0, 0.125, 1, 0, 0.125, 0.75, 0, 0.125, 0.5, 0, 0.125, 0.25, 0, 0.125, 0, 0, 0, 1, 0, 0, 0.75, 0, 0, 0.5, 0, 0, 0.25, 0, 0, 0, 0, 1, 1, 0, 0.875, 1, 0, 0.875, 0.75, 0, 1, 0.75, 0, 0.875, 0.5, 0, 1, 0.5, 0, 0.875, 0.25, 0, 1, 0.25, 0, 0.875, 0, 0, 1, 0, 0, 0.75, 1, 0, 0.75, 0.75, 0, 0.75, 0.5, 0, 0.75, 0.25, 0, 0.75, 0, 0, 0.625, 1, 0, 0.625, 0.75, 0, 0.625, 0.5, 0, 0.625, 0.25, 0, 0.625, 0, 0, 0.5, 1, 0, 0.5, 0.75, 0, 0.5, 0.5, 0, 0.5, 0.25, 0, 0.5, 0, 0, 0.5, 1, 0, 0.375, 1, 0, 0.375, 0.75, 0, 0.5, 0.75, 0, 0.375, 0.5, 0, 0.5, 0.5, 0, 0.375, 0.25, 0, 0.5, 0.25, 0, 0.375, 0, 0, 0.5, 0, 0, 0.25, 1, 0, 0.25, 0.75, 0, 0.25, 0.5, 0, 0.25, 0.25, 0, 0.25, 0, 0, 0.125, 1, 0, 0.125, 0.75, 0, 0.125, 0.5, 0, 0.125, 0.25, 0, 0.125, 0, 0, 0, 1, 0, 0, 0.75, 0, 0, 0.5, 0, 0, 0.25, 0, 0, 0, 0 ]); var teapotIndices = new Uint16Array([ 0, 1, 2, 2, 3, 0, 3, 2, 4, 4, 5, 3, 5, 4, 6, 6, 7, 5, 7, 6, 8, 8, 9, 7, 1, 10, 11, 11, 2, 1, 2, 11, 12, 12, 4, 2, 4, 12, 13, 13, 6, 4, 6, 13, 14, 14, 8, 6, 10, 15, 16, 16, 11, 10, 11, 16, 17, 17, 12, 11, 12, 17, 18, 18, 13, 12, 13, 18, 19, 19, 14, 13, 15, 20, 21, 21, 16, 15, 16, 21, 22, 22, 17, 16, 17, 22, 23, 23, 18, 17, 18, 23, 24, 24, 19, 18, 25, 26, 27, 27, 28, 25, 28, 27, 29, 29, 30, 28, 30, 29, 31, 31, 32, 30, 32, 31, 33, 33, 34, 32, 26, 35, 36, 36, 27, 26, 27, 36, 37, 37, 29, 27, 29, 37, 38, 38, 31, 29, 31, 38, 39, 39, 33, 31, 35, 40, 41, 41, 36, 35, 36, 41, 42, 42, 37, 36, 37, 42, 43, 43, 38, 37, 38, 43, 44, 44, 39, 38, 40, 45, 46, 46, 41, 40, 41, 46, 47, 47, 42, 41, 42, 47, 48, 48, 43, 42, 43, 48, 49, 49, 44, 43, 50, 51, 52, 52, 53, 50, 53, 52, 54, 54, 55, 53, 55, 54, 56, 56, 57, 55, 57, 56, 58, 58, 59, 57, 51, 60, 61, 61, 52, 51, 52, 61, 62, 62, 54, 52, 54, 62, 63, 63, 56, 54, 56, 63, 64, 64, 58, 56, 60, 65, 66, 66, 61, 60, 61, 66, 67, 67, 62, 61, 62, 67, 68, 68, 63, 62, 63, 68, 69, 69, 64, 63, 65, 70, 71, 71, 66, 65, 66, 71, 72, 72, 67, 66, 67, 72, 73, 73, 68, 67, 68, 73, 74, 74, 69, 68, 75, 76, 77, 77, 78, 75, 78, 77, 79, 79, 80, 78, 80, 79, 81, 81, 82, 80, 82, 81, 83, 83, 84, 82, 76, 85, 86, 86, 77, 76, 77, 86, 87, 87, 79, 77, 79, 87, 88, 88, 81, 79, 81, 88, 89, 89, 83, 81, 85, 90, 91, 91, 86, 85, 86, 91, 92, 92, 87, 86, 87, 92, 93, 93, 88, 87, 88, 93, 94, 94, 89, 88, 90, 95, 96, 96, 91, 90, 91, 96, 97, 97, 92, 91, 92, 97, 98, 98, 93, 92, 93, 98, 99, 99, 94, 93, 100, 101, 102, 102, 103, 100, 103, 102, 104, 104, 105, 103, 105, 104, 106, 106, 107, 105, 107, 106, 108, 108, 109, 107, 101, 110, 111, 111, 102, 101, 102, 111, 112, 112, 104, 102, 104, 112, 113, 113, 106, 104, 106, 113, 114, 114, 108, 106, 110, 115, 116, 116, 111, 110, 111, 116, 117, 117, 112, 111, 112, 117, 118, 118, 113, 112, 113, 118, 119, 119, 114, 113, 115, 120, 121, 121, 116, 115, 116, 121, 122, 122, 117, 116, 117, 122, 123, 123, 118, 117, 118, 123, 124, 124, 119, 118, 125, 126, 127, 127, 128, 125, 128, 127, 129, 129, 130, 128, 130, 129, 131, 131, 132, 130, 132, 131, 133, 133, 134, 132, 126, 135, 136, 136, 127, 126, 127, 136, 137, 137, 129, 127, 129, 137, 138, 138, 131, 129, 131, 138, 139, 139, 133, 131, 135, 140, 141, 141, 136, 135, 136, 141, 142, 142, 137, 136, 137, 142, 143, 143, 138, 137, 138, 143, 144, 144, 139, 138, 140, 145, 146, 146, 141, 140, 141, 146, 147, 147, 142, 141, 142, 147, 148, 148, 143, 142, 143, 148, 149, 149, 144, 143, 150, 151, 152, 152, 153, 150, 153, 152, 154, 154, 155, 153, 155, 154, 156, 156, 157, 155, 157, 156, 158, 158, 159, 157, 151, 160, 161, 161, 152, 151, 152, 161, 162, 162, 154, 152, 154, 162, 163, 163, 156, 154, 156, 163, 164, 164, 158, 156, 160, 165, 166, 166, 161, 160, 161, 166, 167, 167, 162, 161, 162, 167, 168, 168, 163, 162, 163, 168, 169, 169, 164, 163, 165, 170, 171, 171, 166, 165, 166, 171, 172, 172, 167, 166, 167, 172, 173, 173, 168, 167, 168, 173, 174, 174, 169, 168, 175, 176, 177, 177, 178, 175, 178, 177, 179, 179, 180, 178, 180, 179, 181, 181, 182, 180, 182, 181, 183, 183, 184, 182, 176, 185, 186, 186, 177, 176, 177, 186, 187, 187, 179, 177, 179, 187, 188, 188, 181, 179, 181, 188, 189, 189, 183, 181, 185, 190, 191, 191, 186, 185, 186, 191, 192, 192, 187, 186, 187, 192, 193, 193, 188, 187, 188, 193, 194, 194, 189, 188, 190, 195, 196, 196, 191, 190, 191, 196, 197, 197, 192, 191, 192, 197, 198, 198, 193, 192, 193, 198, 199, 199, 194, 193, 200, 201, 202, 202, 203, 200, 203, 202, 204, 204, 205, 203, 205, 204, 206, 206, 207, 205, 207, 206, 208, 208, 209, 207, 201, 210, 211, 211, 202, 201, 202, 211, 212, 212, 204, 202, 204, 212, 213, 213, 206, 204, 206, 213, 214, 214, 208, 206, 210, 215, 216, 216, 211, 210, 211, 216, 217, 217, 212, 211, 212, 217, 218, 218, 213, 212, 213, 218, 219, 219, 214, 213, 215, 220, 221, 221, 216, 215, 216, 221, 222, 222, 217, 216, 217, 222, 223, 223, 218, 217, 218, 223, 224, 224, 219, 218, 225, 226, 227, 227, 228, 225, 228, 227, 229, 229, 230, 228, 230, 229, 231, 231, 232, 230, 232, 231, 233, 233, 234, 232, 226, 235, 236, 236, 227, 226, 227, 236, 237, 237, 229, 227, 229, 237, 238, 238, 231, 229, 231, 238, 239, 239, 233, 231, 235, 240, 241, 241, 236, 235, 236, 241, 242, 242, 237, 236, 237, 242, 243, 243, 238, 237, 238, 243, 244, 244, 239, 238, 240, 245, 246, 246, 241, 240, 241, 246, 247, 247, 242, 241, 242, 247, 248, 248, 243, 242, 243, 248, 249, 249, 244, 243, 250, 251, 252, 252, 253, 250, 253, 252, 254, 254, 255, 253, 255, 254, 256, 256, 257, 255, 257, 256, 258, 258, 259, 257, 251, 260, 261, 261, 252, 251, 252, 261, 262, 262, 254, 252, 254, 262, 263, 263, 256, 254, 256, 263, 264, 264, 258, 256, 260, 265, 266, 266, 261, 260, 261, 266, 267, 267, 262, 261, 262, 267, 268, 268, 263, 262, 263, 268, 269, 269, 264, 263, 265, 270, 271, 271, 266, 265, 266, 271, 272, 272, 267, 266, 267, 272, 273, 273, 268, 267, 268, 273, 274, 274, 269, 268, 275, 276, 277, 277, 278, 275, 278, 277, 279, 279, 280, 278, 280, 279, 281, 281, 282, 280, 282, 281, 283, 283, 284, 282, 276, 285, 286, 286, 277, 276, 277, 286, 287, 287, 279, 277, 279, 287, 288, 288, 281, 279, 281, 288, 289, 289, 283, 281, 285, 290, 291, 291, 286, 285, 286, 291, 292, 292, 287, 286, 287, 292, 293, 293, 288, 287, 288, 293, 294, 294, 289, 288, 290, 295, 296, 296, 291, 290, 291, 296, 297, 297, 292, 291, 292, 297, 298, 298, 293, 292, 293, 298, 299, 299, 294, 293, 300, 301, 302, 302, 303, 300, 303, 302, 304, 304, 305, 303, 305, 304, 306, 306, 307, 305, 307, 306, 308, 308, 309, 307, 301, 310, 311, 311, 302, 301, 302, 311, 312, 312, 304, 302, 304, 312, 313, 313, 306, 304, 306, 313, 314, 314, 308, 306, 310, 315, 316, 316, 311, 310, 311, 316, 317, 317, 312, 311, 312, 317, 318, 318, 313, 312, 313, 318, 319, 319, 314, 313, 315, 320, 321, 321, 316, 315, 316, 321, 322, 322, 317, 316, 317, 322, 323, 323, 318, 317, 318, 323, 324, 324, 319, 318, 325, 326, 327, 327, 328, 325, 328, 327, 329, 329, 330, 328, 330, 329, 331, 331, 332, 330, 332, 331, 333, 333, 334, 332, 326, 335, 336, 336, 327, 326, 327, 336, 337, 337, 329, 327, 329, 337, 338, 338, 331, 329, 331, 338, 339, 339, 333, 331, 335, 340, 341, 341, 336, 335, 336, 341, 342, 342, 337, 336, 337, 342, 343, 343, 338, 337, 338, 343, 344, 344, 339, 338, 340, 345, 346, 346, 341, 340, 341, 346, 347, 347, 342, 341, 342, 347, 348, 348, 343, 342, 343, 348, 349, 349, 344, 343, 350, 351, 352, 352, 353, 350, 353, 352, 354, 354, 355, 353, 355, 354, 356, 356, 357, 355, 357, 356, 358, 358, 359, 357, 351, 360, 361, 361, 352, 351, 352, 361, 362, 362, 354, 352, 354, 362, 363, 363, 356, 354, 356, 363, 364, 364, 358, 356, 360, 365, 366, 366, 361, 360, 361, 366, 367, 367, 362, 361, 362, 367, 368, 368, 363, 362, 363, 368, 369, 369, 364, 363, 365, 370, 371, 371, 366, 365, 366, 371, 372, 372, 367, 366, 367, 372, 373, 373, 368, 367, 368, 373, 374, 374, 369, 368, 375, 376, 377, 377, 378, 375, 378, 377, 379, 379, 380, 378, 380, 379, 381, 381, 382, 380, 382, 381, 383, 383, 384, 382, 376, 385, 386, 386, 377, 376, 377, 386, 387, 387, 379, 377, 379, 387, 388, 388, 381, 379, 381, 388, 389, 389, 383, 381, 385, 390, 391, 391, 386, 385, 386, 391, 392, 392, 387, 386, 387, 392, 393, 393, 388, 387, 388, 393, 394, 394, 389, 388, 390, 395, 396, 396, 391, 390, 391, 396, 397, 397, 392, 391, 392, 397, 398, 398, 393, 392, 393, 398, 399, 399, 394, 393, 400, 401, 402, 402, 403, 400, 403, 402, 404, 404, 405, 403, 405, 404, 406, 406, 407, 405, 407, 406, 408, 408, 409, 407, 401, 410, 411, 411, 402, 401, 402, 411, 412, 412, 404, 402, 404, 412, 413, 413, 406, 404, 406, 413, 414, 414, 408, 406, 410, 415, 416, 416, 411, 410, 411, 416, 417, 417, 412, 411, 412, 417, 418, 418, 413, 412, 413, 418, 419, 419, 414, 413, 415, 420, 421, 421, 416, 415, 416, 421, 422, 422, 417, 416, 417, 422, 423, 423, 418, 417, 418, 423, 424, 424, 419, 418, 425, 426, 427, 427, 428, 425, 428, 427, 429, 429, 430, 428, 430, 429, 431, 431, 432, 430, 432, 431, 433, 433, 434, 432, 426, 435, 436, 436, 427, 426, 427, 436, 437, 437, 429, 427, 429, 437, 438, 438, 431, 429, 431, 438, 439, 439, 433, 431, 435, 440, 441, 441, 436, 435, 436, 441, 442, 442, 437, 436, 437, 442, 443, 443, 438, 437, 438, 443, 444, 444, 439, 438, 440, 445, 446, 446, 441, 440, 441, 446, 447, 447, 442, 441, 442, 447, 448, 448, 443, 442, 443, 448, 449, 449, 444, 443, 450, 451, 452, 452, 453, 450, 453, 452, 454, 454, 455, 453, 455, 454, 456, 456, 457, 455, 457, 456, 458, 458, 459, 457, 451, 460, 461, 461, 452, 451, 452, 461, 462, 462, 454, 452, 454, 462, 463, 463, 456, 454, 456, 463, 464, 464, 458, 456, 460, 465, 466, 466, 461, 460, 461, 466, 467, 467, 462, 461, 462, 467, 468, 468, 463, 462, 463, 468, 469, 469, 464, 463, 465, 470, 471, 471, 466, 465, 466, 471, 472, 472, 467, 466, 467, 472, 473, 473, 468, 467, 468, 473, 474, 474, 469, 468, 475, 476, 477, 477, 478, 475, 478, 477, 479, 479, 480, 478, 480, 479, 481, 481, 482, 480, 482, 481, 483, 483, 484, 482, 476, 485, 486, 486, 477, 476, 477, 486, 487, 487, 479, 477, 479, 487, 488, 488, 481, 479, 481, 488, 489, 489, 483, 481, 485, 490, 491, 491, 486, 485, 486, 491, 492, 492, 487, 486, 487, 492, 493, 493, 488, 487, 488, 493, 494, 494, 489, 488, 490, 495, 496, 496, 491, 490, 491, 496, 497, 497, 492, 491, 492, 497, 498, 498, 493, 492, 493, 498, 499, 499, 494, 493, 500, 501, 502, 502, 503, 500, 503, 502, 504, 504, 505, 503, 505, 504, 506, 506, 507, 505, 507, 506, 508, 508, 509, 507, 501, 510, 511, 511, 502, 501, 502, 511, 512, 512, 504, 502, 504, 512, 513, 513, 506, 504, 506, 513, 514, 514, 508, 506, 510, 515, 516, 516, 511, 510, 511, 516, 517, 517, 512, 511, 512, 517, 518, 518, 513, 512, 513, 518, 519, 519, 514, 513, 515, 520, 521, 521, 516, 515, 516, 521, 522, 522, 517, 516, 517, 522, 523, 523, 518, 517, 518, 523, 524, 524, 519, 518, 525, 526, 527, 527, 528, 525, 528, 527, 529, 529, 530, 528, 530, 529, 531, 531, 532, 530, 532, 531, 533, 533, 534, 532, 526, 535, 536, 536, 527, 526, 527, 536, 537, 537, 529, 527, 529, 537, 538, 538, 531, 529, 531, 538, 539, 539, 533, 531, 535, 540, 541, 541, 536, 535, 536, 541, 542, 542, 537, 536, 537, 542, 543, 543, 538, 537, 538, 543, 544, 544, 539, 538, 540, 545, 546, 546, 541, 540, 541, 546, 547, 547, 542, 541, 542, 547, 548, 548, 543, 542, 543, 548, 549, 549, 544, 543, 550, 551, 552, 552, 553, 550, 553, 552, 554, 554, 555, 553, 555, 554, 556, 556, 557, 555, 557, 556, 558, 558, 559, 557, 551, 560, 561, 561, 552, 551, 552, 561, 562, 562, 554, 552, 554, 562, 563, 563, 556, 554, 556, 563, 564, 564, 558, 556, 560, 565, 566, 566, 561, 560, 561, 566, 567, 567, 562, 561, 562, 567, 568, 568, 563, 562, 563, 568, 569, 569, 564, 563, 565, 570, 571, 571, 566, 565, 566, 571, 572, 572, 567, 566, 567, 572, 573, 573, 568, 567, 568, 573, 574, 574, 569, 568, 575, 576, 577, 577, 578, 575, 578, 577, 579, 579, 580, 578, 580, 579, 581, 581, 582, 580, 582, 581, 583, 583, 584, 582, 576, 585, 586, 586, 577, 576, 577, 586, 587, 587, 579, 577, 579, 587, 588, 588, 581, 579, 581, 588, 589, 589, 583, 581, 585, 590, 591, 591, 586, 585, 586, 591, 592, 592, 587, 586, 587, 592, 593, 593, 588, 587, 588, 593, 594, 594, 589, 588, 590, 595, 596, 596, 591, 590, 591, 596, 597, 597, 592, 591, 592, 597, 598, 598, 593, 592, 593, 598, 599, 599, 594, 593, 600, 601, 602, 602, 603, 600, 603, 602, 604, 604, 605, 603, 605, 604, 606, 606, 607, 605, 607, 606, 608, 608, 609, 607, 601, 610, 611, 611, 602, 601, 602, 611, 612, 612, 604, 602, 604, 612, 613, 613, 606, 604, 606, 613, 614, 614, 608, 606, 610, 615, 616, 616, 611, 610, 611, 616, 617, 617, 612, 611, 612, 617, 618, 618, 613, 612, 613, 618, 619, 619, 614, 613, 615, 620, 621, 621, 616, 615, 616, 621, 622, 622, 617, 616, 617, 622, 623, 623, 618, 617, 618, 623, 624, 624, 619, 618, 625, 626, 627, 627, 628, 625, 628, 627, 629, 629, 630, 628, 630, 629, 631, 631, 632, 630, 632, 631, 633, 633, 634, 632, 626, 635, 636, 636, 627, 626, 627, 636, 637, 637, 629, 627, 629, 637, 638, 638, 631, 629, 631, 638, 639, 639, 633, 631, 635, 640, 641, 641, 636, 635, 636, 641, 642, 642, 637, 636, 637, 642, 643, 643, 638, 637, 638, 643, 644, 644, 639, 638, 640, 645, 646, 646, 641, 640, 641, 646, 647, 647, 642, 641, 642, 647, 648, 648, 643, 642, 643, 648, 649, 649, 644, 643, 650, 651, 652, 652, 653, 650, 653, 652, 654, 654, 655, 653, 655, 654, 656, 656, 657, 655, 657, 656, 658, 658, 659, 657, 651, 660, 661, 661, 652, 651, 652, 661, 662, 662, 654, 652, 654, 662, 663, 663, 656, 654, 656, 663, 664, 664, 658, 656, 660, 665, 666, 666, 661, 660, 661, 666, 667, 667, 662, 661, 662, 667, 668, 668, 663, 662, 663, 668, 669, 669, 664, 663, 665, 670, 671, 671, 666, 665, 666, 671, 672, 672, 667, 666, 667, 672, 673, 673, 668, 667, 668, 673, 674, 674, 669, 668, 675, 676, 677, 677, 678, 675, 678, 677, 679, 679, 680, 678, 680, 679, 681, 681, 682, 680, 682, 681, 683, 683, 684, 682, 676, 685, 686, 686, 677, 676, 677, 686, 687, 687, 679, 677, 679, 687, 688, 688, 681, 679, 681, 688, 689, 689, 683, 681, 685, 690, 691, 691, 686, 685, 686, 691, 692, 692, 687, 686, 687, 692, 693, 693, 688, 687, 688, 693, 694, 694, 689, 688, 690, 695, 696, 696, 691, 690, 691, 696, 697, 697, 692, 691, 692, 697, 698, 698, 693, 692, 693, 698, 699, 699, 694, 693, 700, 701, 702, 702, 703, 700, 703, 702, 704, 704, 705, 703, 705, 704, 706, 706, 707, 705, 707, 706, 708, 708, 709, 707, 701, 710, 711, 711, 702, 701, 702, 711, 712, 712, 704, 702, 704, 712, 713, 713, 706, 704, 706, 713, 714, 714, 708, 706, 710, 715, 716, 716, 711, 710, 711, 716, 717, 717, 712, 711, 712, 717, 718, 718, 713, 712, 713, 718, 719, 719, 714, 713, 715, 720, 721, 721, 716, 715, 716, 721, 722, 722, 717, 716, 717, 722, 723, 723, 718, 717, 718, 723, 724, 724, 719, 718, 725, 726, 727, 727, 728, 725, 728, 727, 729, 729, 730, 728, 730, 729, 731, 731, 732, 730, 732, 731, 733, 733, 734, 732, 726, 735, 736, 736, 727, 726, 727, 736, 737, 737, 729, 727, 729, 737, 738, 738, 731, 729, 731, 738, 739, 739, 733, 731, 735, 740, 741, 741, 736, 735, 736, 741, 742, 742, 737, 736, 737, 742, 743, 743, 738, 737, 738, 743, 744, 744, 739, 738, 740, 745, 746, 746, 741, 740, 741, 746, 747, 747, 742, 741, 742, 747, 748, 748, 743, 742, 743, 748, 749, 749, 744, 743, 750, 751, 752, 752, 753, 750, 753, 752, 754, 754, 755, 753, 755, 754, 756, 756, 757, 755, 757, 756, 758, 758, 759, 757, 751, 760, 761, 761, 752, 751, 752, 761, 762, 762, 754, 752, 754, 762, 763, 763, 756, 754, 756, 763, 764, 764, 758, 756, 760, 765, 766, 766, 761, 760, 761, 766, 767, 767, 762, 761, 762, 767, 768, 768, 763, 762, 763, 768, 769, 769, 764, 763, 765, 770, 771, 771, 766, 765, 766, 771, 772, 772, 767, 766, 767, 772, 773, 773, 768, 767, 768, 773, 774, 774, 769, 768, 775, 776, 777, 777, 778, 775, 778, 777, 779, 779, 780, 778, 780, 779, 781, 781, 782, 780, 782, 781, 783, 783, 784, 782, 776, 785, 786, 786, 777, 776, 777, 786, 787, 787, 779, 777, 779, 787, 788, 788, 781, 779, 781, 788, 789, 789, 783, 781, 785, 790, 791, 791, 786, 785, 786, 791, 792, 792, 787, 786, 787, 792, 793, 793, 788, 787, 788, 793, 794, 794, 789, 788, 790, 795, 796, 796, 791, 790, 791, 796, 797, 797, 792, 791, 792, 797, 798, 798, 793, 792, 793, 798, 799, 799, 794, 793 ]); ================================================ FILE: src/js/third_party/webgl_teapot/webgl-debug.js ================================================ /* ** Copyright (c) 2012 The Khronos Group Inc. ** ** Permission is hereby granted, free of charge, to any person obtaining a ** copy of this software and/or associated documentation files (the ** "Materials"), to deal in the Materials without restriction, including ** without limitation the rights to use, copy, modify, merge, publish, ** distribute, sublicense, and/or sell copies of the Materials, and to ** permit persons to whom the Materials are furnished to do so, subject to ** the following conditions: ** ** The above copyright notice and this permission notice shall be included ** in all copies or substantial portions of the Materials. ** ** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE ** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. */ // Various functions for helping debug WebGL apps. WebGLDebugUtils = function() { /** * Wrapped logging function. * @param {string} msg Message to log. */ var log = function(msg) { if (window.console && window.console.log) { window.console.log(msg); } }; /** * Wrapped error logging function. * @param {string} msg Message to log. */ var error = function(msg) { if (window.console && window.console.error) { window.console.error(msg); } else { log(msg); } }; /** * Which arguments are enums based on the number of arguments to the function. * So * 'texImage2D': { * 9: { 0:true, 2:true, 6:true, 7:true }, * 6: { 0:true, 2:true, 3:true, 4:true }, * }, * * means if there are 9 arguments then 6 and 7 are enums, if there are 6 * arguments 3 and 4 are enums * * @type {!Object.} */ var glValidEnumContexts = { // Generic setters and getters 'enable': {1: { 0:true }}, 'disable': {1: { 0:true }}, 'getParameter': {1: { 0:true }}, // Rendering 'drawArrays': {3:{ 0:true }}, 'drawElements': {4:{ 0:true, 2:true }}, // Shaders 'createShader': {1: { 0:true }}, 'getShaderParameter': {2: { 1:true }}, 'getProgramParameter': {2: { 1:true }}, 'getShaderPrecisionFormat': {2: { 0: true, 1:true }}, // Vertex attributes 'getVertexAttrib': {2: { 1:true }}, 'vertexAttribPointer': {6: { 2:true }}, // Textures 'bindTexture': {2: { 0:true }}, 'activeTexture': {1: { 0:true }}, 'getTexParameter': {2: { 0:true, 1:true }}, 'texParameterf': {3: { 0:true, 1:true }}, 'texParameteri': {3: { 0:true, 1:true, 2:true }}, 'texImage2D': { 9: { 0:true, 2:true, 6:true, 7:true }, 6: { 0:true, 2:true, 3:true, 4:true } }, 'texSubImage2D': { 9: { 0:true, 6:true, 7:true }, 7: { 0:true, 4:true, 5:true } }, 'copyTexImage2D': {8: { 0:true, 2:true }}, 'copyTexSubImage2D': {8: { 0:true }}, 'generateMipmap': {1: { 0:true }}, 'compressedTexImage2D': {7: { 0: true, 2:true }}, 'compressedTexSubImage2D': {8: { 0: true, 6:true }}, // Buffer objects 'bindBuffer': {2: { 0:true }}, 'bufferData': {3: { 0:true, 2:true }}, 'bufferSubData': {3: { 0:true }}, 'getBufferParameter': {2: { 0:true, 1:true }}, // Renderbuffers and framebuffers 'pixelStorei': {2: { 0:true, 1:true }}, 'readPixels': {7: { 4:true, 5:true }}, 'bindRenderbuffer': {2: { 0:true }}, 'bindFramebuffer': {2: { 0:true }}, 'checkFramebufferStatus': {1: { 0:true }}, 'framebufferRenderbuffer': {4: { 0:true, 1:true, 2:true }}, 'framebufferTexture2D': {5: { 0:true, 1:true, 2:true }}, 'getFramebufferAttachmentParameter': {3: { 0:true, 1:true, 2:true }}, 'getRenderbufferParameter': {2: { 0:true, 1:true }}, 'renderbufferStorage': {4: { 0:true, 1:true }}, // Frame buffer operations (clear, blend, depth test, stencil) 'clear': {1: { 0: { 'enumBitwiseOr': ['COLOR_BUFFER_BIT', 'DEPTH_BUFFER_BIT', 'STENCIL_BUFFER_BIT'] }}}, 'depthFunc': {1: { 0:true }}, 'blendFunc': {2: { 0:true, 1:true }}, 'blendFuncSeparate': {4: { 0:true, 1:true, 2:true, 3:true }}, 'blendEquation': {1: { 0:true }}, 'blendEquationSeparate': {2: { 0:true, 1:true }}, 'stencilFunc': {3: { 0:true }}, 'stencilFuncSeparate': {4: { 0:true, 1:true }}, 'stencilMaskSeparate': {2: { 0:true }}, 'stencilOp': {3: { 0:true, 1:true, 2:true }}, 'stencilOpSeparate': {4: { 0:true, 1:true, 2:true, 3:true }}, // Culling 'cullFace': {1: { 0:true }}, 'frontFace': {1: { 0:true }}, // ANGLE_instanced_arrays extension 'drawArraysInstancedANGLE': {4: { 0:true }}, 'drawElementsInstancedANGLE': {5: { 0:true, 2:true }}, // EXT_blend_minmax extension 'blendEquationEXT': {1: { 0:true }} }; /** * Map of numbers to names. * @type {Object} */ var glEnums = null; /** * Map of names to numbers. * @type {Object} */ var enumStringToValue = null; /** * Initializes this module. Safe to call more than once. * @param {!WebGLRenderingContext} ctx A WebGL context. If * you have more than one context it doesn't matter which one * you pass in, it is only used to pull out constants. */ function init(ctx) { if (glEnums == null) { glEnums = { }; enumStringToValue = { }; for (var propertyName in ctx) { if (typeof ctx[propertyName] == 'number') { glEnums[ctx[propertyName]] = propertyName; enumStringToValue[propertyName] = ctx[propertyName]; } } } } /** * Checks the utils have been initialized. */ function checkInit() { if (glEnums == null) { throw 'WebGLDebugUtils.init(ctx) not called'; } } /** * Returns true or false if value matches any WebGL enum * @param {*} value Value to check if it might be an enum. * @return {boolean} True if value matches one of the WebGL defined enums */ function mightBeEnum(value) { checkInit(); return (glEnums[value] !== undefined); } /** * Gets an string version of an WebGL enum. * * Example: * var str = WebGLDebugUtil.glEnumToString(ctx.getError()); * * @param {number} value Value to return an enum for * @return {string} The string version of the enum. */ function glEnumToString(value) { checkInit(); var name = glEnums[value]; return (name !== undefined) ? ("gl." + name) : ("/*UNKNOWN WebGL ENUM*/ 0x" + value.toString(16) + ""); } /** * Returns the string version of a WebGL argument. * Attempts to convert enum arguments to strings. * @param {string} functionName the name of the WebGL function. * @param {number} numArgs the number of arguments passed to the function. * @param {number} argumentIndx the index of the argument. * @param {*} value The value of the argument. * @return {string} The value as a string. */ function glFunctionArgToString(functionName, numArgs, argumentIndex, value) { var funcInfo = glValidEnumContexts[functionName]; if (funcInfo !== undefined) { var funcInfo = funcInfo[numArgs]; if (funcInfo !== undefined) { if (funcInfo[argumentIndex]) { if (typeof funcInfo[argumentIndex] === 'object' && funcInfo[argumentIndex]['enumBitwiseOr'] !== undefined) { var enums = funcInfo[argumentIndex]['enumBitwiseOr']; var orResult = 0; var orEnums = []; for (var i = 0; i < enums.length; ++i) { var enumValue = enumStringToValue[enums[i]]; if ((value & enumValue) !== 0) { orResult |= enumValue; orEnums.push(glEnumToString(enumValue)); } } if (orResult === value) { return orEnums.join(' | '); } else { return glEnumToString(value); } } else { return glEnumToString(value); } } } } if (value === null) { return "null"; } else if (value === undefined) { return "undefined"; } else { return value.toString(); } } /** * Converts the arguments of a WebGL function to a string. * Attempts to convert enum arguments to strings. * * @param {string} functionName the name of the WebGL function. * @param {number} args The arguments. * @return {string} The arguments as a string. */ function glFunctionArgsToString(functionName, args) { // apparently we can't do args.join(","); var argStr = ""; var numArgs = args.length; for (var ii = 0; ii < numArgs; ++ii) { argStr += ((ii == 0) ? '' : ', ') + glFunctionArgToString(functionName, numArgs, ii, args[ii]); } return argStr; }; function makePropertyWrapper(wrapper, original, propertyName) { //log("wrap prop: " + propertyName); wrapper.__defineGetter__(propertyName, function() { return original[propertyName]; }); // TODO(gmane): this needs to handle properties that take more than // one value? wrapper.__defineSetter__(propertyName, function(value) { //log("set: " + propertyName); original[propertyName] = value; }); } // Makes a function that calls a function on another object. function makeFunctionWrapper(original, functionName) { //log("wrap fn: " + functionName); var f = original[functionName]; return function() { //log("call: " + functionName); var result = f.apply(original, arguments); return result; }; } /** * Given a WebGL context returns a wrapped context that calls * gl.getError after every command and calls a function if the * result is not gl.NO_ERROR. * * @param {!WebGLRenderingContext} ctx The webgl context to * wrap. * @param {!function(err, funcName, args): void} opt_onErrorFunc * The function to call when gl.getError returns an * error. If not specified the default function calls * console.log with a message. * @param {!function(funcName, args): void} opt_onFunc The * function to call when each webgl function is called. * You can use this to log all calls for example. * @param {!WebGLRenderingContext} opt_err_ctx The webgl context * to call getError on if different than ctx. */ function makeDebugContext(ctx, opt_onErrorFunc, opt_onFunc, opt_err_ctx) { opt_err_ctx = opt_err_ctx || ctx; init(ctx); opt_onErrorFunc = opt_onErrorFunc || function(err, functionName, args) { // apparently we can't do args.join(","); var argStr = ""; var numArgs = args.length; for (var ii = 0; ii < numArgs; ++ii) { argStr += ((ii == 0) ? '' : ', ') + glFunctionArgToString(functionName, numArgs, ii, args[ii]); } error("WebGL error "+ glEnumToString(err) + " in "+ functionName + "(" + argStr + ")"); }; // Holds booleans for each GL error so after we get the error ourselves // we can still return it to the client app. var glErrorShadow = { }; // Makes a function that calls a WebGL function and then calls getError. function makeErrorWrapper(ctx, functionName) { return function() { if (opt_onFunc) { opt_onFunc(functionName, arguments); } var result = ctx[functionName].apply(ctx, arguments); var err = opt_err_ctx.getError(); if (err != 0) { glErrorShadow[err] = true; opt_onErrorFunc(err, functionName, arguments); } return result; }; } // Make a an object that has a copy of every property of the WebGL context // but wraps all functions. var wrapper = {}; for (var propertyName in ctx) { if (typeof ctx[propertyName] == 'function') { if (propertyName != 'getExtension') { wrapper[propertyName] = makeErrorWrapper(ctx, propertyName); } else { var wrapped = makeErrorWrapper(ctx, propertyName); wrapper[propertyName] = function () { var result = wrapped.apply(ctx, arguments); return makeDebugContext(result, opt_onErrorFunc, opt_onFunc, opt_err_ctx); }; } } else { makePropertyWrapper(wrapper, ctx, propertyName); } } // Override the getError function with one that returns our saved results. wrapper.getError = function() { for (var err in glErrorShadow) { if (glErrorShadow.hasOwnProperty(err)) { if (glErrorShadow[err]) { glErrorShadow[err] = false; return err; } } } return ctx.NO_ERROR; }; return wrapper; } function resetToInitialState(ctx) { var numAttribs = ctx.getParameter(ctx.MAX_VERTEX_ATTRIBS); var tmp = ctx.createBuffer(); ctx.bindBuffer(ctx.ARRAY_BUFFER, tmp); for (var ii = 0; ii < numAttribs; ++ii) { ctx.disableVertexAttribArray(ii); ctx.vertexAttribPointer(ii, 4, ctx.FLOAT, false, 0, 0); ctx.vertexAttrib1f(ii, 0); } ctx.deleteBuffer(tmp); var numTextureUnits = ctx.getParameter(ctx.MAX_TEXTURE_IMAGE_UNITS); for (var ii = 0; ii < numTextureUnits; ++ii) { ctx.activeTexture(ctx.TEXTURE0 + ii); ctx.bindTexture(ctx.TEXTURE_CUBE_MAP, null); ctx.bindTexture(ctx.TEXTURE_2D, null); } ctx.activeTexture(ctx.TEXTURE0); ctx.useProgram(null); ctx.bindBuffer(ctx.ARRAY_BUFFER, null); ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, null); ctx.bindFramebuffer(ctx.FRAMEBUFFER, null); ctx.bindRenderbuffer(ctx.RENDERBUFFER, null); ctx.disable(ctx.BLEND); ctx.disable(ctx.CULL_FACE); ctx.disable(ctx.DEPTH_TEST); ctx.disable(ctx.DITHER); ctx.disable(ctx.SCISSOR_TEST); ctx.blendColor(0, 0, 0, 0); ctx.blendEquation(ctx.FUNC_ADD); ctx.blendFunc(ctx.ONE, ctx.ZERO); ctx.clearColor(0, 0, 0, 0); ctx.clearDepth(1); ctx.clearStencil(-1); ctx.colorMask(true, true, true, true); ctx.cullFace(ctx.BACK); ctx.depthFunc(ctx.LESS); ctx.depthMask(true); ctx.depthRange(0, 1); ctx.frontFace(ctx.CCW); ctx.hint(ctx.GENERATE_MIPMAP_HINT, ctx.DONT_CARE); ctx.lineWidth(1); ctx.pixelStorei(ctx.PACK_ALIGNMENT, 4); ctx.pixelStorei(ctx.UNPACK_ALIGNMENT, 4); ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, false); ctx.pixelStorei(ctx.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); // TODO: Delete this IF. if (ctx.UNPACK_COLORSPACE_CONVERSION_WEBGL) { ctx.pixelStorei(ctx.UNPACK_COLORSPACE_CONVERSION_WEBGL, ctx.BROWSER_DEFAULT_WEBGL); } ctx.polygonOffset(0, 0); ctx.sampleCoverage(1, false); ctx.scissor(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.stencilFunc(ctx.ALWAYS, 0, 0xFFFFFFFF); ctx.stencilMask(0xFFFFFFFF); ctx.stencilOp(ctx.KEEP, ctx.KEEP, ctx.KEEP); ctx.viewport(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT | ctx.STENCIL_BUFFER_BIT); // TODO: This should NOT be needed but Firefox fails with 'hint' while(ctx.getError()); } function makeLostContextSimulatingCanvas(canvas) { var unwrappedContext_; var wrappedContext_; var onLost_ = []; var onRestored_ = []; var wrappedContext_ = {}; var contextId_ = 1; var contextLost_ = false; var resourceId_ = 0; var resourceDb_ = []; var numCallsToLoseContext_ = 0; var numCalls_ = 0; var canRestore_ = false; var restoreTimeout_ = 0; // Holds booleans for each GL error so can simulate errors. var glErrorShadow_ = { }; canvas.getContext = function(f) { return function() { var ctx = f.apply(canvas, arguments); // Did we get a context and is it a WebGL context? if (ctx instanceof WebGLRenderingContext) { if (ctx != unwrappedContext_) { if (unwrappedContext_) { throw "got different context" } unwrappedContext_ = ctx; wrappedContext_ = makeLostContextSimulatingContext(unwrappedContext_); } return wrappedContext_; } return ctx; } }(canvas.getContext); function wrapEvent(listener) { if (typeof(listener) == "function") { return listener; } else { return function(info) { listener.handleEvent(info); } } } var addOnContextLostListener = function(listener) { onLost_.push(wrapEvent(listener)); }; var addOnContextRestoredListener = function(listener) { onRestored_.push(wrapEvent(listener)); }; function wrapAddEventListener(canvas) { var f = canvas.addEventListener; canvas.addEventListener = function(type, listener, bubble) { switch (type) { case 'webglcontextlost': addOnContextLostListener(listener); break; case 'webglcontextrestored': addOnContextRestoredListener(listener); break; default: f.apply(canvas, arguments); } }; } wrapAddEventListener(canvas); canvas.loseContext = function() { if (!contextLost_) { contextLost_ = true; numCallsToLoseContext_ = 0; ++contextId_; while (unwrappedContext_.getError()); clearErrors(); glErrorShadow_[unwrappedContext_.CONTEXT_LOST_WEBGL] = true; var event = makeWebGLContextEvent("context lost"); var callbacks = onLost_.slice(); setTimeout(function() { //log("numCallbacks:" + callbacks.length); for (var ii = 0; ii < callbacks.length; ++ii) { //log("calling callback:" + ii); callbacks[ii](event); } if (restoreTimeout_ >= 0) { setTimeout(function() { canvas.restoreContext(); }, restoreTimeout_); } }, 0); } }; canvas.restoreContext = function() { if (contextLost_) { if (onRestored_.length) { setTimeout(function() { if (!canRestore_) { throw "can not restore. webglcontestlost listener did not call event.preventDefault"; } freeResources(); resetToInitialState(unwrappedContext_); contextLost_ = false; numCalls_ = 0; canRestore_ = false; var callbacks = onRestored_.slice(); var event = makeWebGLContextEvent("context restored"); for (var ii = 0; ii < callbacks.length; ++ii) { callbacks[ii](event); } }, 0); } } }; canvas.loseContextInNCalls = function(numCalls) { if (contextLost_) { throw "You can not ask a lost contet to be lost"; } numCallsToLoseContext_ = numCalls_ + numCalls; }; canvas.getNumCalls = function() { return numCalls_; }; canvas.setRestoreTimeout = function(timeout) { restoreTimeout_ = timeout; }; function isWebGLObject(obj) { //return false; return (obj instanceof WebGLBuffer || obj instanceof WebGLFramebuffer || obj instanceof WebGLProgram || obj instanceof WebGLRenderbuffer || obj instanceof WebGLShader || obj instanceof WebGLTexture); } function checkResources(args) { for (var ii = 0; ii < args.length; ++ii) { var arg = args[ii]; if (isWebGLObject(arg)) { return arg.__webglDebugContextLostId__ == contextId_; } } return true; } function clearErrors() { var k = Object.keys(glErrorShadow_); for (var ii = 0; ii < k.length; ++ii) { delete glErrorShadow_[k]; } } function loseContextIfTime() { ++numCalls_; if (!contextLost_) { if (numCallsToLoseContext_ == numCalls_) { canvas.loseContext(); } } } // Makes a function that simulates WebGL when out of context. function makeLostContextFunctionWrapper(ctx, functionName) { var f = ctx[functionName]; return function() { // log("calling:" + functionName); // Only call the functions if the context is not lost. loseContextIfTime(); if (!contextLost_) { //if (!checkResources(arguments)) { // glErrorShadow_[wrappedContext_.INVALID_OPERATION] = true; // return; //} var result = f.apply(ctx, arguments); return result; } }; } function freeResources() { for (var ii = 0; ii < resourceDb_.length; ++ii) { var resource = resourceDb_[ii]; if (resource instanceof WebGLBuffer) { unwrappedContext_.deleteBuffer(resource); } else if (resource instanceof WebGLFramebuffer) { unwrappedContext_.deleteFramebuffer(resource); } else if (resource instanceof WebGLProgram) { unwrappedContext_.deleteProgram(resource); } else if (resource instanceof WebGLRenderbuffer) { unwrappedContext_.deleteRenderbuffer(resource); } else if (resource instanceof WebGLShader) { unwrappedContext_.deleteShader(resource); } else if (resource instanceof WebGLTexture) { unwrappedContext_.deleteTexture(resource); } } } function makeWebGLContextEvent(statusMessage) { return { statusMessage: statusMessage, preventDefault: function() { canRestore_ = true; } }; } return canvas; function makeLostContextSimulatingContext(ctx) { // copy all functions and properties to wrapper for (var propertyName in ctx) { if (typeof ctx[propertyName] == 'function') { wrappedContext_[propertyName] = makeLostContextFunctionWrapper( ctx, propertyName); } else { makePropertyWrapper(wrappedContext_, ctx, propertyName); } } // Wrap a few functions specially. wrappedContext_.getError = function() { loseContextIfTime(); if (!contextLost_) { var err; while (err = unwrappedContext_.getError()) { glErrorShadow_[err] = true; } } for (var err in glErrorShadow_) { if (glErrorShadow_[err]) { delete glErrorShadow_[err]; return err; } } return wrappedContext_.NO_ERROR; }; var creationFunctions = [ "createBuffer", "createFramebuffer", "createProgram", "createRenderbuffer", "createShader", "createTexture" ]; for (var ii = 0; ii < creationFunctions.length; ++ii) { var functionName = creationFunctions[ii]; wrappedContext_[functionName] = function(f) { return function() { loseContextIfTime(); if (contextLost_) { return null; } var obj = f.apply(ctx, arguments); obj.__webglDebugContextLostId__ = contextId_; resourceDb_.push(obj); return obj; }; }(ctx[functionName]); } var functionsThatShouldReturnNull = [ "getActiveAttrib", "getActiveUniform", "getBufferParameter", "getContextAttributes", "getAttachedShaders", "getFramebufferAttachmentParameter", "getParameter", "getProgramParameter", "getProgramInfoLog", "getRenderbufferParameter", "getShaderParameter", "getShaderInfoLog", "getShaderSource", "getTexParameter", "getUniform", "getUniformLocation", "getVertexAttrib" ]; for (var ii = 0; ii < functionsThatShouldReturnNull.length; ++ii) { var functionName = functionsThatShouldReturnNull[ii]; wrappedContext_[functionName] = function(f) { return function() { loseContextIfTime(); if (contextLost_) { return null; } return f.apply(ctx, arguments); } }(wrappedContext_[functionName]); } var isFunctions = [ "isBuffer", "isEnabled", "isFramebuffer", "isProgram", "isRenderbuffer", "isShader", "isTexture" ]; for (var ii = 0; ii < isFunctions.length; ++ii) { var functionName = isFunctions[ii]; wrappedContext_[functionName] = function(f) { return function() { loseContextIfTime(); if (contextLost_) { return false; } return f.apply(ctx, arguments); } }(wrappedContext_[functionName]); } wrappedContext_.checkFramebufferStatus = function(f) { return function() { loseContextIfTime(); if (contextLost_) { return wrappedContext_.FRAMEBUFFER_UNSUPPORTED; } return f.apply(ctx, arguments); }; }(wrappedContext_.checkFramebufferStatus); wrappedContext_.getAttribLocation = function(f) { return function() { loseContextIfTime(); if (contextLost_) { return -1; } return f.apply(ctx, arguments); }; }(wrappedContext_.getAttribLocation); wrappedContext_.getVertexAttribOffset = function(f) { return function() { loseContextIfTime(); if (contextLost_) { return 0; } return f.apply(ctx, arguments); }; }(wrappedContext_.getVertexAttribOffset); wrappedContext_.isContextLost = function() { return contextLost_; }; return wrappedContext_; } } return { /** * Initializes this module. Safe to call more than once. * @param {!WebGLRenderingContext} ctx A WebGL context. If * you have more than one context it doesn't matter which one * you pass in, it is only used to pull out constants. */ 'init': init, /** * Returns true or false if value matches any WebGL enum * @param {*} value Value to check if it might be an enum. * @return {boolean} True if value matches one of the WebGL defined enums */ 'mightBeEnum': mightBeEnum, /** * Gets an string version of an WebGL enum. * * Example: * WebGLDebugUtil.init(ctx); * var str = WebGLDebugUtil.glEnumToString(ctx.getError()); * * @param {number} value Value to return an enum for * @return {string} The string version of the enum. */ 'glEnumToString': glEnumToString, /** * Converts the argument of a WebGL function to a string. * Attempts to convert enum arguments to strings. * * Example: * WebGLDebugUtil.init(ctx); * var str = WebGLDebugUtil.glFunctionArgToString('bindTexture', 2, 0, gl.TEXTURE_2D); * * would return 'TEXTURE_2D' * * @param {string} functionName the name of the WebGL function. * @param {number} numArgs The number of arguments * @param {number} argumentIndx the index of the argument. * @param {*} value The value of the argument. * @return {string} The value as a string. */ 'glFunctionArgToString': glFunctionArgToString, /** * Converts the arguments of a WebGL function to a string. * Attempts to convert enum arguments to strings. * * @param {string} functionName the name of the WebGL function. * @param {number} args The arguments. * @return {string} The arguments as a string. */ 'glFunctionArgsToString': glFunctionArgsToString, /** * Given a WebGL context returns a wrapped context that calls * gl.getError after every command and calls a function if the * result is not NO_ERROR. * * You can supply your own function if you want. For example, if you'd like * an exception thrown on any GL error you could do this * * function throwOnGLError(err, funcName, args) { * throw WebGLDebugUtils.glEnumToString(err) + * " was caused by call to " + funcName; * }; * * ctx = WebGLDebugUtils.makeDebugContext( * canvas.getContext("webgl"), throwOnGLError); * * @param {!WebGLRenderingContext} ctx The webgl context to wrap. * @param {!function(err, funcName, args): void} opt_onErrorFunc The function * to call when gl.getError returns an error. If not specified the default * function calls console.log with a message. * @param {!function(funcName, args): void} opt_onFunc The * function to call when each webgl function is called. You * can use this to log all calls for example. */ 'makeDebugContext': makeDebugContext, /** * Given a canvas element returns a wrapped canvas element that will * simulate lost context. The canvas returned adds the following functions. * * loseContext: * simulates a lost context event. * * restoreContext: * simulates the context being restored. * * lostContextInNCalls: * loses the context after N gl calls. * * getNumCalls: * tells you how many gl calls there have been so far. * * setRestoreTimeout: * sets the number of milliseconds until the context is restored * after it has been lost. Defaults to 0. Pass -1 to prevent * automatic restoring. * * @param {!Canvas} canvas The canvas element to wrap. */ 'makeLostContextSimulatingCanvas': makeLostContextSimulatingCanvas, /** * Resets a context to the initial state. * @param {!WebGLRenderingContext} ctx The webgl context to * reset. */ 'resetToInitialState': resetToInitialState }; }(); ================================================ FILE: src/js/third_party/webgl_teapot/webgl-utils.js ================================================ /* * Copyright 2010, Google Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @fileoverview This file contains functions every webgl program will need * a version of one way or another. * * Instead of setting up a context manually it is recommended to * use. This will check for success or failure. On failure it * will attempt to present an approriate message to the user. * * gl = WebGLUtils.setupWebGL(canvas); * * For animated WebGL apps use of setTimeout or setInterval are * discouraged. It is recommended you structure your rendering * loop like this. * * function render() { * window.requestAnimFrame(render, canvas); * * // do rendering * ... * } * render(); * * This will call your rendering function up to the refresh rate * of your display but will stop rendering if your app is not * visible. */ WebGLUtils = function() { /** * Creates the HTLM for a failure message * @param {string} canvasContainerId id of container of th * canvas. * @return {string} The html. */ var makeFailHTML = function(msg) { return '' + '' + '
' + '
' + '
' + msg + '
' + '
' + '
'; }; /** * Mesasge for getting a webgl browser * @type {string} */ var GET_A_WEBGL_BROWSER = '' + 'This page requires a browser that supports WebGL.
' + 'Click here to upgrade your browser.'; /** * Mesasge for need better hardware * @type {string} */ var OTHER_PROBLEM = '' + "It doesn't appear your computer can support WebGL.
" + 'Click here for more information.'; /** * Creates a webgl context. If creation fails it will * change the contents of the container of the * tag to an error message with the correct links for WebGL. * @param {Element} canvas. The canvas element to create a * context from. * @param {WebGLContextCreationAttirbutes} opt_attribs Any * creation attributes you want to pass in. * @return {WebGLRenderingContext} The created context. */ var setupWebGL = function(canvas, opt_attribs) { function showLink(str) { var container = canvas.parentNode; if (container) { container.innerHTML = makeFailHTML(str); } }; if (!window.WebGLRenderingContext) { showLink(GET_A_WEBGL_BROWSER); return null; } var context = create3DContext(canvas, opt_attribs); if (!context) { showLink(OTHER_PROBLEM); } return context; }; /** * Creates a webgl context. * @param {!Canvas} canvas The canvas tag to get context * from. If one is not passed in one will be created. * @return {!WebGLContext} The created context. */ var create3DContext = function(canvas, opt_attribs) { var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"]; var context = null; for (var ii = 0; ii < names.length; ++ii) { try { context = canvas.getContext(names[ii], opt_attribs); } catch(e) {} if (context) { break; } } return context; }; return { create3DContext: create3DContext, setupWebGL: setupWebGL }; }(); /** * Provides requestAnimationFrame in a cross browser way. */ window.requestAnimFrame = (function() { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(/* function FrameRequestCallback */ callback, /* DOMElement Element */ element) { return window.setTimeout(callback, 1000/60); }; })(); /** * Provides cancelAnimationFrame in a cross browser way. */ window.cancelAnimFrame = (function() { return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.oCancelAnimationFrame || window.msCancelAnimationFrame || window.clearTimeout; })(); ================================================ FILE: src/js/videopipe.js ================================================ /* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ // // A "videopipe" abstraction on top of WebRTC. // // The usage of this abstraction: // var pipe = new VideoPipe(mediastream, handlerFunction); // handlerFunction = function(mediastream) { // do_something // } // pipe.close(); // // The VideoPipe will set up 2 PeerConnections, connect them to each // other, and call HandlerFunction when the stream is available in the // second PeerConnection. // function errorHandler(context) { return function(error) { trace('Failure in ' + context + ': ' + error.toString); }; } // eslint-disable-next-line no-unused-vars function successHandler(context) { return function() { trace('Success in ' + context); }; } function noAction() { } function VideoPipe(stream, handler) { let servers = null; let pc1 = new RTCPeerConnection(servers); let pc2 = new RTCPeerConnection(servers); pc1.addStream(stream); pc1.onicecandidate = function(event) { if (event.candidate) { pc2.addIceCandidate(new RTCIceCandidate(event.candidate), noAction, errorHandler('AddIceCandidate')); } }; pc2.onicecandidate = function(event) { if (event.candidate) { pc1.addIceCandidate(new RTCIceCandidate(event.candidate), noAction, errorHandler('AddIceCandidate')); } }; pc2.onaddstream = function(e) { handler(e.stream); }; pc1.createOffer(function(desc) { pc1.setLocalDescription(desc); pc2.setRemoteDescription(desc); pc2.createAnswer(function(desc2) { pc2.setLocalDescription(desc2); pc1.setRemoteDescription(desc2); }, errorHandler('pc2.createAnswer')); }, errorHandler('pc1.createOffer')); this.pc1 = pc1; this.pc2 = pc2; } VideoPipe.prototype.close = function() { this.pc1.close(); this.pc2.close(); }; ================================================ FILE: test/download-browsers.js ================================================ const {buildDriver} = require('./webdriver'); // Download the browser(s). async function download() { if (process.env.BROWSER_A && process.env.BROWSER_B) { (await buildDriver(process.env.BROWSER_A)).quit(); (await buildDriver(process.env.BROWSER_B)).quit(); } else { (await buildDriver()).quit(); } } download(); ================================================ FILE: test/interop/connection.test.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ const {buildDriver} = require('../webdriver'); const {PeerConnection, MediaDevices} = require('../webrtcclient'); const steps = require('../steps'); const browserA = process.env.BROWSER_A || 'chrome'; const browserB = process.env.BROWSER_B || 'chrome'; describe(`basic interop test ${browserA} => ${browserB}`, function() { let drivers; let clients; beforeAll(async () => { const options = { version: process.env.BVER || 'stable', browserLogging: true, } drivers = [ await buildDriver(browserA, options), await buildDriver(browserB, options), ]; clients = drivers.map(driver => { return { connection: new PeerConnection(driver), mediaDevices: new MediaDevices(driver), }; }); }); afterAll(async () => { await drivers.map(driver => driver.close()); }); it('establishes a connection', async () => { await Promise.all(drivers); // timeouts in before(Each)? await steps.step(drivers, (d) => d.get('https://webrtc.github.io/samples/emptypage.html'), 'Empty page loaded'); await steps.step(clients, (client) => client.connection.create(), 'Created RTCPeerConnection'); await steps.step(clients, async (client) => { const stream = await client.mediaDevices.getUserMedia({audio: true, video: true}); return Promise.all(stream.getTracks().map(async track => { return client.connection.addTrack(track, stream); })); }, 'Acquired and added audio/video stream'); const offerWithCandidates = await clients[0].connection.setLocalDescription(); await clients[1].connection.setRemoteDescription(offerWithCandidates); const answerWithCandidates = await clients[1].connection.setLocalDescription(); await clients[0].connection.setRemoteDescription(answerWithCandidates); await steps.step(drivers, (d) => steps.waitNVideosExist(d, 1), 'Video elements exist'); await steps.step(drivers, steps.waitAllVideosHaveEnoughData, 'Video elements have enough data'); }, 30000); }, 90000); ================================================ FILE: test/steps.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ const TIMEOUT = 10000; function step(drivers, cb, logMessage) { return Promise.all(drivers.map(driver => { return cb(driver); })).then(() => { if (logMessage) { console.log(logMessage); } }); } function waitNVideosExist(driver, n) { return driver.wait(() => { return driver.executeScript(n => document.querySelectorAll('video').length === n, n); }, TIMEOUT); } function waitAllVideosHaveEnoughData(driver) { return driver.wait(() => { return driver.executeScript(() => { const videos = document.querySelectorAll('video'); let ready = 0; for (let i = 0; i < videos.length; i++) { if (videos[i].readyState >= videos[i].HAVE_ENOUGH_DATA) { ready++; } } return ready === videos.length; }); }, TIMEOUT); } module.exports = { step, waitNVideosExist, waitAllVideosHaveEnoughData, }; ================================================ FILE: test/webdriver.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ const os = require('os'); const path = require('path'); const webdriver = require('selenium-webdriver'); const chrome = require('selenium-webdriver/chrome'); const firefox = require('selenium-webdriver/firefox'); const safari = require('selenium-webdriver/safari'); const puppeteerBrowsers = require('@puppeteer/browsers'); async function download(browser, version, cacheDir, platform) { const buildId = await puppeteerBrowsers .resolveBuildId(browser, platform, version); await puppeteerBrowsers.install({ browser, buildId, cacheDir, platform }); return buildId; } const cacheDir = path.join(process.cwd(), 'browsers'); if (os.platform() === 'win32') { process.env.PATH += ';' + process.cwd() + '\\node_modules\\chromedriver\\lib\\chromedriver\\'; process.env.PATH += ';' + process.cwd() + '\\node_modules\\geckodriver'; } else { process.env.PATH += ':node_modules/.bin'; } function mapVersion(browser, version) { const versionMap = { chrome: { unstable: 'canary', }, firefox: { unstable: 'nightly', } }; return (versionMap[browser] || {})[version] || version; } async function buildDriver(browser = process.env.BROWSER || 'chrome', options = {version: process.env.BVER}) { const version = mapVersion(browser, options.version); const platform = puppeteerBrowsers.detectBrowserPlatform(); const buildId = await download(browser, version || 'stable', cacheDir, platform); // Chrome options. const chromeOptions = new chrome.Options() .addArguments('allow-insecure-localhost') .addArguments('use-fake-device-for-media-stream') .addArguments('allow-file-access-from-files'); if (options.chromeFlags) { options.chromeFlags.forEach((flag) => chromeOptions.addArguments(flag)); } if (options.chromepath) { chromeOptions.setChromeBinaryPath(options.chromepath); } else { chromeOptions.setChromeBinaryPath(puppeteerBrowsers .computeExecutablePath({browser, buildId, cacheDir, platform})); } if (!options.devices || options.headless) { // GUM doesn't work in headless mode so we need this. See // https://bugs.chromium.org/p/chromium/issues/detail?id=776649 chromeOptions.addArguments('use-fake-ui-for-media-stream'); } else { // see https://bugs.chromium.org/p/chromium/issues/detail?id=459532#c22 const domain = 'https://' + (options.devices.domain || 'localhost') + ':' + (options.devices.port || 443) + ',*'; const exceptions = { media_stream_mic: {}, media_stream_camera: {}, }; exceptions.media_stream_mic[domain] = { last_used: Date.now(), setting: options.devices.audio ? 1 : 2 // 0: ask, 1: allow, 2: denied }; exceptions.media_stream_camera[domain] = { last_used: Date.now(), setting: options.devices.video ? 1 : 2 }; chromeOptions.setUserPreferences({ profile: { content_settings: { exceptions: exceptions } } }); } // Safari options. const safariOptions = new safari.Options(); safariOptions.setTechnologyPreview(version === 'unstable'); // Firefox options. const firefoxOptions = new firefox.Options(); let firefoxPath = firefox.Channel.RELEASE; if (options.firefoxpath) { firefoxPath = options.firefoxpath; } else { firefoxPath = puppeteerBrowsers .computeExecutablePath({browser, buildId, cacheDir, platform}); } if (options.headless) { firefoxOptions.addArguments('-headless'); } firefoxOptions.setBinary(firefoxPath); firefoxOptions.setPreference('media.navigator.streams.fake', true); firefoxOptions.setPreference('media.navigator.permission.disabled', true); const driver = new webdriver.Builder() .setChromeOptions(chromeOptions) .setSafariOptions(safariOptions) .setFirefoxOptions(firefoxOptions) .forBrowser(browser) .setChromeService( new chrome.ServiceBuilder().addArguments('--disable-build-check') ); if (browser === 'firefox') { driver.getCapabilities().set('marionette', true); driver.getCapabilities().set('acceptInsecureCerts', true); } return driver.build(); } module.exports = { buildDriver, }; ================================================ FILE: test/webrtcclient.js ================================================ /* * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ // Disable no-undef since this file is a mix of code executed // in JS and the browser. /* eslint no-undef: 0 */ class MediaStream { constructor(tracks = []) { this.tracks = tracks; this.id = 0; } getTracks() { return this.tracks; } getAudioTracks() { return this.getTracks().filter(t => t.kind === 'audio'); } getVideoTracks() { return this.getTracks().filter(t => t.kind === 'video'); } } class MediaDevices { constructor(driver) { this.driver = driver; } getUserMedia(constraints) { return this.driver.executeAsyncScript((constraints) => { const callback = arguments[arguments.length - 1]; if (!window.localStreams) { window.localStreams = {}; } return navigator.mediaDevices.getUserMedia(constraints) .then((stream) => { window.localStreams[stream.id] = stream; callback({id: stream.id, tracks: stream.getTracks().map((t) => { return {id: t.id, kind: t.kind}; })}); }, (e) => callback(e)); }, constraints || {audio: true, video: true}) .then((streamObj) => { const stream = new MediaStream(streamObj.tracks); stream.id = streamObj.id; return stream; }); } } class PeerConnection { constructor(driver) { this.driver = driver; } create(rtcConfiguration) { return this.driver.executeScript(rtcConfiguration => { window.pc = new RTCPeerConnection(rtcConfiguration); }, rtcConfiguration); } addTrack(track, stream) { return this.driver.executeScript((track, stream) => { stream = localStreams[stream.id]; track = stream.getTracks().find(t => t.id === track.id); pc.addTrack(track, stream); }, track, stream); } createOffer(offerOptions) { return this.driver.executeAsyncScript((offerOptions) => { const callback = arguments[arguments.length - 1]; pc.createOffer(offerOptions) .then(callback, callback); }, offerOptions); } createAnswer() { return this.driver.executeAsyncScript(() => { const callback = arguments[arguments.length - 1]; pc.createAnswer() .then(callback, callback); }); } // resolves with non-trickle description including candidates. setLocalDescription(desc) { return this.driver.executeAsyncScript((desc) => { const callback = arguments[arguments.length - 1]; pc.onicecandidate = (event) => { console.log('candidate', event.candidate); if (!event.candidate) { pc.onicecandidate = null; callback(pc.localDescription); } }; pc.setLocalDescription(desc) .catch(callback); }, desc); } // TODO: this implicitly creates video elements, is that deseriable? setRemoteDescription(desc) { return this.driver.executeAsyncScript(function(desc) { const callback = arguments[arguments.length - 1]; pc.ontrack = function(event) { const id = event.streams[0].id; if (document.getElementById('video-' + id)) { return; } const video = document.createElement('video'); video.id = 'video-' + id; video.autoplay = true; video.srcObject = event.streams[0]; document.body.appendChild(video); }; pc.setRemoteDescription(new RTCSessionDescription(desc)) .then(callback, callback); }, desc); } } module.exports = { PeerConnection, MediaDevices, MediaStream, };