1/* 2 * Copyright 2017 The Chromium Authors. All rights reserved. 3 * Use of this source code is governed by a BSD-style license that can be 4 * found in the LICENSE file. 5 */ 6/*jshint esversion: 6 */ 7 8'use strict'; 9 10const $ = document.getElementById.bind(document); 11 12function logError(err) { 13 console.error(err); 14} 15 16/** 17 * FeedTable stores all elements. 18 */ 19class FeedTable { 20 constructor() { 21 this.numCols = 5; 22 this.col = 0; 23 this.testTable = $('test-table'); 24 this.row = this.testTable.insertRow(-1); 25 } 26 27 addNewCell(elementType) { 28 if (this.col == this.numCols) { 29 this.row = this.testTable.insertRow(-1); 30 this.col = 0; 31 } 32 var newCell = this.row.insertCell(-1); 33 var element = document.createElement(elementType); 34 element.autoplay = false; 35 newCell.appendChild(element); 36 this.col++; 37 return element; 38 } 39} 40 41/** 42 * A simple loopback connection; 43 * - localConnection is fed video/audio from source 44 * - localConnection is linked to remoteConnection 45 * - remoteConnection is displayed in the given videoElement 46 */ 47class PeerConnection { 48 49 /** 50 * @param {!HTMLMediaElement} element - And 'audio' or 'video' element. 51 * @param {!Object} constraints - The constraints for the peer connection. 52 */ 53 constructor(element, constraints) { 54 this.localConnection = null; 55 this.remoteConnection = null; 56 this.remoteElement = element; 57 this.constraints = constraints; 58 } 59 60 start() { 61 return navigator.mediaDevices 62 .getUserMedia(this.constraints) 63 .then((stream) => { 64 this.onGetUserMediaSuccess(stream) 65 }); 66 }; 67 68 onGetUserMediaSuccess(stream) { 69 this.localConnection = new RTCPeerConnection(null); 70 this.localConnection.onicecandidate = (event) => { 71 this.onIceCandidate(this.remoteConnection, event); 72 }; 73 this.localConnection.addStream(stream); 74 75 this.remoteConnection = new RTCPeerConnection(null); 76 this.remoteConnection.onicecandidate = (event) => { 77 this.onIceCandidate(this.localConnection, event); 78 }; 79 this.remoteConnection.onaddstream = (e) => { 80 this.remoteElement.srcObject = e.stream; 81 }; 82 83 this.localConnection 84 .createOffer({offerToReceiveAudio: 1, offerToReceiveVideo: 1}) 85 .then((desc) => {this.onCreateOfferSuccess(desc)}, logError); 86 }; 87 88 onCreateOfferSuccess(desc) { 89 this.localConnection.setLocalDescription(desc); 90 this.remoteConnection.setRemoteDescription(desc); 91 92 this.remoteConnection.createAnswer().then( 93 (desc) => {this.onCreateAnswerSuccess(desc)}, logError); 94 }; 95 96 onCreateAnswerSuccess(desc) { 97 this.remoteConnection.setLocalDescription(desc); 98 this.localConnection.setRemoteDescription(desc); 99 }; 100 101 onIceCandidate(connection, event) { 102 if (event.candidate) { 103 connection.addIceCandidate(new RTCIceCandidate(event.candidate)); 104 } 105 }; 106} 107 108 109class TestRunner { 110 constructor(runtimeSeconds, pausePlayIterationDelayMillis) { 111 this.runtimeSeconds = runtimeSeconds; 112 this.pausePlayIterationDelayMillis = pausePlayIterationDelayMillis; 113 this.elements = []; 114 this.peerConnections = []; 115 this.feedTable = new FeedTable(); 116 this.iteration = 0; 117 this.startTime; 118 this.lastIterationTime; 119 } 120 121 addPeerConnection(elementType) { 122 const element = this.feedTable.addNewCell(elementType); 123 const constraints = {audio: true}; 124 if (elementType === 'video') { 125 constraints.video = { 126 width: {exact: 300} 127 }; 128 } else if (elementType === 'audio') { 129 constraints.video = false; 130 } else { 131 throw new Error('elementType must be one of "audio" or "video"'); 132 } 133 this.elements.push(element); 134 this.peerConnections.push(new PeerConnection(element, constraints)); 135 } 136 137 startTest() { 138 this.startTime = Date.now(); 139 let promises = testRunner.peerConnections.map((conn) => conn.start()); 140 Promise.all(promises) 141 .then(() => { 142 this.startTime = Date.now(); 143 this.pauseAndPlayLoop(); 144 }) 145 .catch((e) => {throw e}); 146 } 147 148 pauseAndPlayLoop() { 149 this.iteration++; 150 this.elements.forEach((feed) => { 151 if (Math.random() >= 0.5) { 152 feed.play(); 153 } else { 154 feed.pause(); 155 } 156 }); 157 const status = this.getStatus(); 158 this.lastIterationTime = Date.now(); 159 $('status').textContent = status 160 if (status != 'ok-done') { 161 setTimeout( 162 () => {this.pauseAndPlayLoop()}, this.pausePlayIterationDelayMillis); 163 } else { // We're done. Pause all feeds. 164 this.elements.forEach((feed) => { 165 feed.pause(); 166 }); 167 } 168 } 169 170 getStatus() { 171 if (this.iteration == 0) { 172 return 'not-started'; 173 } 174 const timeSpent = Date.now() - this.startTime; 175 if (timeSpent >= this.runtimeSeconds * 1000) { 176 return 'ok-done'; 177 } else { 178 return `running, iteration: ${this.iteration}`; 179 } 180 } 181 182 getResults() { 183 const runTimeMillis = this.lastIterationTime - this.startTime; 184 return {'runTimeSeconds': runTimeMillis / 1000}; 185 } 186} 187 188let testRunner; 189 190function startTest( 191 runtimeSeconds, numPeerConnections, pausePlayIterationDelayMillis, 192 elementType) { 193 testRunner = new TestRunner( 194 runtimeSeconds, pausePlayIterationDelayMillis); 195 for (let i = 0; i < numPeerConnections; i++) { 196 testRunner.addPeerConnection(elementType); 197 } 198 testRunner.startTest(); 199} 200