├── gen └── .keep ├── .gitignore ├── .env_sample ├── readme-img1.png ├── composer.json ├── src ├── assets │ ├── js │ │ ├── test_browser │ │ │ ├── utils │ │ │ │ ├── status.js │ │ │ │ ├── webrtc_utils.js │ │ │ │ └── bowser.js │ │ │ ├── test_cases │ │ │ │ ├── test_browser.js │ │ │ │ ├── test_micro.js │ │ │ │ ├── test_devices.js │ │ │ │ ├── test_camera.js │ │ │ │ ├── test_room.js │ │ │ │ └── test_network.js │ │ │ ├── JitsiTestEvent.js │ │ │ ├── Statistics.js │ │ │ ├── TestResults.js │ │ │ ├── ui.js │ │ │ └── JitsiTestBrowser.js │ │ ├── lang.js │ │ └── app.js │ └── css │ │ ├── animations.css │ │ └── app.css ├── lang │ ├── en.php │ └── fr.php └── index.html ├── docker-compose.yml ├── docker ├── webrtctest.conf └── entrypoint.sh ├── Dockerfile ├── README.md ├── composer.lock ├── LICENSE └── scripts └── make-release.php /gen/.keep: -------------------------------------------------------------------------------- 1 | Here so git can track the folder -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gen/** 2 | !gen/.keep 3 | vendor/** 4 | src/debug/** 5 | .env -------------------------------------------------------------------------------- /.env_sample: -------------------------------------------------------------------------------- 1 | #Web server 2 | SERVER_NAME=127.0.0.1:8443 3 | TURN_CRED_ENDPOINT=XXXXXXXXX -------------------------------------------------------------------------------- /readme-img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimoc/JitsiMeetTestBrowserTool/main/readme-img1.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "matthiasmullie/minify": "^1.3" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/js/test_browser/utils/status.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum for test running statuses 3 | * 4 | * @type {{PAUSED: number, STOPPED: number, PROCESSING: number, WAITING: number, ENDED: number}} 5 | */ 6 | window.TestStatuses = { 7 | WAITING: 0, 8 | PROCESSING: 1, 9 | PAUSED: 2, 10 | STOPPED: 3, 11 | ENDED: 4, 12 | }; -------------------------------------------------------------------------------- /src/assets/js/lang.js: -------------------------------------------------------------------------------- 1 | window.lang = { 2 | dictionary: "", 3 | 4 | get: function(key){ 5 | if(this.dictionary[key] !== undefined) { 6 | return this.dictionary[key] 7 | }else{ 8 | console.error(`Translation not found: ${key}`) 9 | return `{${key}}`; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | jwt_server: 3 | image: jitsimeettestbrowsertool 4 | build: 5 | context: jitsimeettestbrowsertool 6 | container_name: jitsimeettestbrowsertool 7 | ports: 8 | - "8080:80" 9 | - "8443:443" 10 | logging: 11 | driver: syslog 12 | options: 13 | tag: "jitsimeettestbrowsertool" 14 | env_file: 15 | - .env 16 | -------------------------------------------------------------------------------- /src/assets/css/animations.css: -------------------------------------------------------------------------------- 1 | .progress-container { 2 | height: 0.2rem; 3 | width: 90%; 4 | border-radius: 0.4rem; 5 | 6 | background: #35476b; 7 | } 8 | 9 | .progress-container .progress { 10 | height: 100%; 11 | width: 0; 12 | border-radius: 0.4rem; 13 | 14 | background: #039be5; 15 | 16 | transition: width 0.4s ease; 17 | } 18 | 19 | /* Blink */ 20 | .blink { 21 | animation: blinker 1s linear infinite; 22 | } 23 | 24 | @keyframes blinker { 25 | 50% { 26 | opacity: 0; 27 | } 28 | } -------------------------------------------------------------------------------- /docker/webrtctest.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName ${SERVER_NAME} 3 | 4 | SSLEngine on 5 | SSLCertificateFile /etc/apache2/apache.pem 6 | 7 | alias /WebRTCTest /usr/local/WebRTCTestBrowser/gen/1.0.0/index.html 8 | 9 | 10 | 11 | AuthType None 12 | Require all granted 13 | 14 | 15 | 16 | AuthType None 17 | Require all granted 18 | 19 | 20 | LogLevel warn 21 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONFIG_DIR="/usr/local/WebRTCTestBrowser" 4 | export config='$config' 5 | echo "ServerName $SERVER_NAME" 6 | 7 | echo "Generating config File from template and .env file" 8 | 9 | echo "Apache" 10 | cat $CONFIG_DIR/webrtctest.conf | envsubst > /etc/apache2/sites-available/webrtctest.conf 11 | 12 | echo "Config Generation Done" 13 | 14 | echo "Dealing with certificate files" 15 | if [ ! -f /etc/apache2/apache.pem ] 16 | then 17 | echo "Generate self signed certificate for apache" 18 | openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 10000 -nodes -subj '/CN='$SERVER_NAME 19 | cat key.pem cert.pem > /etc/apache2/apache.pem 20 | rm key.pem cert.pem 21 | fi 22 | 23 | a2ensite webrtctest.conf && a2enmod ssl 24 | 25 | cd $CONFIG_DIR/scripts 26 | php make-release.php --release 1.0.0 --debug --turn-endpoint $TURN_CRED_ENDPOINT 27 | 28 | #Start Apache 29 | /usr/sbin/apache2ctl -DFOREGROUND 30 | -------------------------------------------------------------------------------- /src/assets/js/test_browser/test_cases/test_browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TestCase: test_browser 3 | */ 4 | 5 | if (!window.hasOwnProperty('JitsiTestBrowser')) 6 | window.JitsiTestBrowser = {}; 7 | 8 | /** 9 | * Test browser case 10 | */ 11 | window.JitsiTestBrowser.test_browser = { 12 | 13 | /** 14 | * Run test 15 | * 16 | * @return {Promise<*>} 17 | */ 18 | run: function () { 19 | return new Promise(res => { 20 | console.log("> Running test_browser"); 21 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.PROCESSING, "context": "test_browser"}); 22 | 23 | let utils = new WebRTCUtils(); 24 | let status = "fail"; 25 | 26 | if (navigator.mediaDevices !== undefined && utils.isPeerConnectionSupported() && !utils.isBannedBrowser()) { 27 | status = "success"; 28 | } 29 | 30 | window.JitsiTestBrowser.runner.resolve(res, {"result": status}, "test_browser"); 31 | }); 32 | } 33 | } -------------------------------------------------------------------------------- /src/assets/js/test_browser/JitsiTestEvent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * List of Jitsi tests events 3 | */ 4 | window.JitsiTestEvents = { 5 | 6 | /** 7 | * Test running event 8 | */ 9 | run: new Event('run'), 10 | 11 | 12 | /** 13 | * Network stat event 14 | */ 15 | networkStat: new Event('network_stat'), 16 | 17 | 18 | dispatch: function(eventName, data = undefined){ 19 | let event; 20 | switch (eventName){ 21 | case 'run': 22 | event = this.run; 23 | break; 24 | 25 | case 'network_stat': 26 | event = this.networkStat; 27 | break; 28 | 29 | default: 30 | console.error(`Unknwon event: ${event}`) 31 | return; 32 | } 33 | 34 | for (const [key, value] of Object.entries(data)) { 35 | event[key] = value; 36 | } 37 | 38 | document.dispatchEvent(event); 39 | 40 | // Reset entities 41 | this[eventName] = new Event(eventName); 42 | } 43 | } -------------------------------------------------------------------------------- /src/assets/js/test_browser/test_cases/test_micro.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TestCase: test_micro 3 | */ 4 | 5 | if (!window.hasOwnProperty('JitsiTestBrowser')) 6 | window.JitsiTestBrowser = {}; 7 | 8 | /** 9 | * Test micro case 10 | */ 11 | window.JitsiTestBrowser.test_micro = { 12 | 13 | /** 14 | * Run test 15 | * 16 | * @return {Promise<*>} 17 | */ 18 | run: function () { 19 | return new Promise(res => { 20 | console.log("> Running test_micro"); 21 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.PROCESSING, "context": "test_micro"}); 22 | 23 | let utils = new WebRTCUtils(); 24 | 25 | utils.getDefaultMediaCapture("audio", function (result) { 26 | window.JitsiTestBrowser.runner.resolve(res, {"result": "success", 'details': result}, "test_micro"); 27 | 28 | }, function (error) { 29 | window.JitsiTestBrowser.runner.resolve(res, {"result": "fail", 'details': error}, "test_micro"); 30 | }); 31 | }); 32 | } 33 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-slim 2 | 3 | RUN apt update && apt-get -y install curl gettext-base\ 4 | && apt-get update\ 5 | && apt-get install -y --install-recommends apache2\ 6 | && apt-get -y install php php-mysql php-mbstring php-gmp composer zip unzip php-zip 7 | 8 | 9 | RUN mkdir /usr/local/WebRTCTestBrowser 10 | COPY ./scripts /usr/local/WebRTCTestBrowser/scripts 11 | COPY ./src /usr/local/WebRTCTestBrowser/src 12 | COPY ./gen /usr/local/WebRTCTestBrowser/gen 13 | COPY ./docker/webrtctest.conf /usr/local/WebRTCTestBrowser/webrtctest.conf 14 | 15 | COPY composer.json /usr/local/WebRTCTestBrowser/composer.json 16 | COPY composer.lock /usr/local/WebRTCTestBrowser/composer.lock 17 | 18 | RUN cd /usr/local/WebRTCTestBrowser\ 19 | && composer install -n \ 20 | && rm -rf /var/lib/apt/lists/* 21 | 22 | EXPOSE 80 443 23 | 24 | # redirect apache logs to docker stdout/stderr 25 | RUN ln -sf /proc/1/fd/1 /var/log/apache2/access.log 26 | RUN ln -sf /proc/1/fd/2 /var/log/apache2/error.log 27 | 28 | COPY ./docker/entrypoint.sh /var/ 29 | RUN chmod +x /var/entrypoint.sh 30 | 31 | ENTRYPOINT ["/bin/bash", "/var/entrypoint.sh"] -------------------------------------------------------------------------------- /src/assets/js/test_browser/test_cases/test_devices.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TestCase: test_devices 3 | */ 4 | 5 | if (!window.hasOwnProperty('JitsiTestBrowser')) 6 | window.JitsiTestBrowser = {}; 7 | 8 | /** 9 | * Test devices case 10 | */ 11 | window.JitsiTestBrowser.test_devices = { 12 | 13 | /** 14 | * Run test 15 | * 16 | * @return {Promise<*>} 17 | */ 18 | run: function () { 19 | return new Promise(resolve => { 20 | console.log("> Running test_devices"); 21 | window.JitsiTestEvents.dispatch('run', { 22 | "status": window.TestStatuses.PROCESSING, 23 | "context": "test_devices" 24 | }); 25 | 26 | let utils = new WebRTCUtils(); 27 | 28 | utils.getListDevices(function (result) { 29 | window.JitsiTestBrowser.runner.resolve(resolve, {"result": "success", 'details': result}, "test_devices"); 30 | 31 | }, function (error) { 32 | window.JitsiTestBrowser.runner.resolve(resolve, {"result": "fail", 'details': error}, "test_devices"); 33 | }); 34 | }); 35 | } 36 | } -------------------------------------------------------------------------------- /src/assets/js/test_browser/test_cases/test_camera.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TestCase: test_camera 3 | */ 4 | 5 | if (!window.hasOwnProperty('JitsiTestBrowser')) 6 | window.JitsiTestBrowser = {}; 7 | 8 | /** 9 | * Test camera case 10 | */ 11 | window.JitsiTestBrowser.test_camera = { 12 | 13 | /** 14 | * Run test 15 | * 16 | * @return {Promise<*>} 17 | */ 18 | run: function () { 19 | return new Promise(res => { 20 | console.log("> Running test_camera"); 21 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.PROCESSING, "context": "test_camera"}); 22 | 23 | let utils = new WebRTCUtils(); 24 | 25 | utils.getDefaultMediaCapture("video", function (result) { 26 | window.JitsiTestBrowser.runner.resolve(res, { 27 | "result": "success", 28 | 'details': result 29 | }, "test_camera") 30 | }, function(error){ 31 | window.JitsiTestBrowser.runner.resolve(res, { 32 | "result": "fail", 33 | 'details': error 34 | }, "test_camera") 35 | }); 36 | }); 37 | } 38 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #

JitsiTestBrowserTool

2 | 3 |
4 | 5 | This tools allows you to make a release for Jitsi test browser page (minify js/css files, pack the app in one file). 6 | 7 |

8 | App screenshot 1 9 |

10 | 11 |
12 | 13 | > #### /!\ Not working yet! 14 | > 15 | > We are currently working on it, stay in touch ;) 16 | 17 | 18 | # Getting started 19 | ## Environment 20 | ### Requirements 21 | 22 | To make a release, you need to have at lease: 23 | * php (7.4+) 24 | * composer (1.10+) 25 | 26 | Then, use **composer install** to get the dependencies. 27 | 28 | 29 | ## Make a new release 30 | 31 | To make a new release, you need to run the **make-release.php** scripts. 32 | 33 | > To get more information, use **"php make-release.php --help"** 34 | 35 | Once done, you will find your unique index.html file containing the test tool. 36 | 37 | 38 | ## Translate the app 39 | 40 | By default, this app is on **english**. 41 | Supported translations are: 42 | * fr (French) 43 | * en (English) 44 | 45 | If you want have your own translation, just copy the **lang/en.php** file into your language (example: **de.php**), then edit the translations. 46 | 47 | Then, re run the make-release.php script with the parameter **--lang=de** 48 | -------------------------------------------------------------------------------- /src/assets/js/test_browser/Statistics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Show test result 3 | */ 4 | 5 | if (!window.hasOwnProperty('JitsiTestBrowser')) 6 | window.JitsiTestBrowser = {}; 7 | 8 | /** 9 | * Show test result 10 | */ 11 | window.JitsiTestBrowser.Statistics = { 12 | 13 | /** 14 | * Statistics content 15 | */ 16 | statistics: {}, 17 | 18 | /** 19 | * Add a statistic 20 | * 21 | * @param testCase 22 | * @param data 23 | */ 24 | addStat: function(testCase, data){ 25 | if (!Object.keys(this.statistics).includes(testCase)){ 26 | this.statistics[testCase] = data; 27 | }else{ 28 | this.statistics[testCase].push(data); 29 | } 30 | }, 31 | 32 | /** 33 | * Export statistics 34 | */ 35 | export: function(){ 36 | // Prepare file name 37 | let current = new Date(); 38 | let file = `${current.getFullYear()}-${current.getMonth()}-${current.getDate()}_${current.getHours()}-${current.getMinutes()}_test-brower-results.json`; 39 | 40 | // Create fake link to force download 41 | let fakeLink = document.createElement('a'); 42 | fakeLink.setAttribute('href', "data:application/json," + encodeURIComponent(JSON.stringify(window.JitsiTestBrowser.Statistics.statistics))); 43 | fakeLink.setAttribute('download', `${file}`); 44 | 45 | document 46 | .querySelector('body') 47 | .append(fakeLink); 48 | 49 | fakeLink.addEventListener('click', function(){ 50 | // Remove fake link 51 | this.remove(); 52 | }); 53 | 54 | // Force download 55 | fakeLink.click(); 56 | }, 57 | 58 | 59 | /** 60 | * Clear statistics 61 | * 62 | * @param testCase 63 | */ 64 | reset: function(testCase = undefined){ 65 | if (testCase === undefined){ 66 | this.statistics = {}; 67 | }else{ 68 | if (this.statistics.hasOwnProperty(testCase)){ 69 | delete this.statistics[testCase] 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/assets/js/test_browser/utils/webrtc_utils.js: -------------------------------------------------------------------------------- 1 | class WebRTCUtils { 2 | 3 | 4 | /** 5 | * Test if RTCPeerConnection API is present 6 | * @returns {boolean} 7 | */ 8 | isPeerConnectionSupported = function () { 9 | let rtcConfig = {}; 10 | try { 11 | return new RTCPeerConnection(rtcConfig) !== undefined; 12 | } catch (err) { 13 | console.log("Peer connection not supported") 14 | return false; 15 | } 16 | } 17 | 18 | /** 19 | * Test if Browser support is part of banned browser 20 | * @returns {boolean} 21 | */ 22 | isBannedBrowser = function () { 23 | return (bowser.msie || bowser.safari || bowser.msedge); 24 | } 25 | 26 | /** 27 | * try to capture a media by its type(audio or video) 28 | * @param {string} mediaType - selected media type : audio|video 29 | * @param {function} success - callback that handles capture track labels 30 | * @param {function} error - callback on error 31 | */ 32 | getDefaultMediaCapture = function (mediaType, success, error) { 33 | const mediaStreamConstraints = { 34 | video: mediaType === "video", 35 | audio: mediaType === "audio" 36 | }; 37 | 38 | navigator.mediaDevices.getUserMedia(mediaStreamConstraints) 39 | .then(function (mediaStream) { 40 | /* use the stream */ 41 | let trackLabels = ""; 42 | mediaStream.getTracks().forEach(function (track) { 43 | trackLabels += track.label; 44 | track.stop(); 45 | }); 46 | success(trackLabels); 47 | }) 48 | .catch(function (err) { 49 | error(err); 50 | }); 51 | } 52 | 53 | 54 | /** 55 | * Get All Media devices seen by the navigator 56 | * 57 | * @param {function} success - callback that handles a mediaInfo object containing media labels 58 | * @param {function} error - callback on error 59 | */ 60 | getListDevices = function(success, error){ 61 | navigator.mediaDevices.enumerateDevices() 62 | .then(function (devices){ 63 | let mediaInfo = { 64 | audioinput:[], 65 | audiooutput:[], 66 | videoinput :[] 67 | }; 68 | console.log(devices); 69 | devices.forEach(function(deviceInfo) { 70 | if (deviceInfo.kind === 'audioinput') 71 | mediaInfo.audioinput.push(deviceInfo.label); 72 | else if (deviceInfo.kind === 'audiooutput') 73 | mediaInfo.audiooutput.push(deviceInfo.label); 74 | else if (deviceInfo.kind === 'videoinput') 75 | mediaInfo.videoinput.push(deviceInfo.label); 76 | }); 77 | success(mediaInfo); 78 | }) 79 | .catch(function(err) { 80 | error(err); 81 | }); 82 | } 83 | 84 | 85 | /** 86 | * Wait function 87 | * 88 | * @param delay Delay to wait 89 | * @return {Promise<*>} 90 | */ 91 | wait = function (delay) { 92 | if (!delay) delay = 1000; 93 | return new Promise(r => setTimeout(r, delay)) 94 | } 95 | } -------------------------------------------------------------------------------- /src/assets/js/test_browser/TestResults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Show test result 3 | */ 4 | 5 | if (!window.hasOwnProperty('JitsiTestBrowser')) 6 | window.JitsiTestBrowser = {}; 7 | 8 | /** 9 | * Show test result 10 | */ 11 | window.JitsiTestBrowser.TestResults = { 12 | 13 | 14 | /** 15 | * List of test cases which did not passed 16 | */ 17 | testsOnError: [], 18 | 19 | /** 20 | * Show test devices results 21 | * 22 | * @param data 23 | */ 24 | test_devices: function(data){ 25 | let container = document.getElementById('devices_results'); 26 | 27 | if (data.result === 'success' && data.details){ 28 | // Success 29 | let sub = container.querySelector('div[data-result="success"]') 30 | sub.classList.remove('hide'); 31 | 32 | // Remove default 33 | container.querySelector('div[data-result="default"]') 34 | .classList.add('hide'); 35 | 36 | ['audioinput', 'audiooutput', 'videoinput'].forEach(function (element){ 37 | if (data.details.hasOwnProperty(element)) { 38 | let elementContainer = sub.querySelector(`div[data-result="${element}"] > div[data-result="content"]`); 39 | elementContainer.innerHTML = ''; 40 | let component = document.createElement('div'); 41 | component.innerHTML = data.details[element].join('
'); 42 | elementContainer.append(component); 43 | } 44 | }); 45 | 46 | }else{ 47 | // Fail 48 | let sub = container.querySelector('div[data-result="fail"]') 49 | sub.classList.remove('hide'); 50 | } 51 | }, 52 | 53 | 54 | /** 55 | * Show final results 56 | */ 57 | test_global: function() { 58 | let container = document.getElementById('global_results'); 59 | 60 | // Remove default 61 | container.querySelector('div[data-result="default"]') 62 | .classList.add('hide'); 63 | 64 | if (window.JitsiTestBrowser.TestResults.testsOnError.length > 0){ 65 | // Got errors 66 | let sub = container.querySelector('div[data-result="fail"]') 67 | sub.classList.remove('hide'); 68 | 69 | // Show test failed 70 | 71 | window.JitsiTestBrowser.TestResults.testsOnError.forEach(testCase => { 72 | sub.querySelector(`[data-test-case="${testCase}"]`).classList.remove('hide'); 73 | }) 74 | }else{ 75 | // All test passed successfully 76 | let sub = container.querySelector('div[data-result="success"]') 77 | sub.classList.remove('hide'); 78 | } 79 | 80 | // Show action buttons 81 | container.querySelector('div[data-content="action_buttons"]').classList.remove('hide'); 82 | }, 83 | 84 | 85 | /** 86 | * Default rendering function 87 | * 88 | * @param testCase 89 | * @param data 90 | */ 91 | defaultRendering: function (testCase, data) { 92 | let container = document.getElementById(document.getElementById(testCase).getAttribute('data-results')); 93 | 94 | // Remove default 95 | container.querySelector('div[data-result="default"]') 96 | .classList.add('hide'); 97 | 98 | if (data.result === 'success'){ 99 | // Success 100 | let sub = container.querySelector('div[data-result="success"]') 101 | sub.classList.remove('hide'); 102 | 103 | // Show result title 104 | sub.querySelector('div[data-result="title"]') 105 | .classList.remove('hide'); 106 | 107 | if (data.details){ 108 | sub.querySelector('div[data-result="data"]') 109 | .append(data.details) 110 | } 111 | 112 | }else{ 113 | // Fail 114 | let sub = container.querySelector('div[data-result="fail"]') 115 | sub.classList.remove('hide'); 116 | } 117 | 118 | container.scrollIntoView({ behavior: 'smooth', block: 'end'}) 119 | } 120 | } -------------------------------------------------------------------------------- /src/lang/en.php: -------------------------------------------------------------------------------- 1 | For the best user experience always use last sable version of your browser.'; 22 | $lang['browser_test_fail']= 'Your browser is not compatible with Jitsi Meet.
Please use one these recommended Browsers.'; 23 | 24 | // Test devices 25 | $lang['devices'] = 'Devices'; 26 | $lang['devices_title'] = 'Test access to your devices'; 27 | $lang['devices_test_success'] = 'Detected media devices :'; 28 | $lang['devices_test_fail']= 'Unable to list your media devices (Audio and Video).'; 29 | $lang['devices_audio_input_label']='Audio input:'; 30 | $lang['devices_audio_output_label']='Audio output:'; 31 | $lang['devices_video_label']='Vidéo input:'; 32 | 33 | // Test camera 34 | $lang['camera'] = 'Camera'; 35 | $lang['camera_title'] = 'Test access to your camera'; 36 | $lang['camera_test_success'] = 'Your camera is correctly detected.'; 37 | $lang['camera_test_success_default'] = 'The camera used by default is:'; 38 | 39 | // Test micro 40 | $lang['micro'] = 'Microphone'; 41 | $lang['micro_title'] = 'Test access to your microphone'; 42 | $lang['micro_test_success'] = 'Your microphone is correctly detected.'; 43 | $lang['micro_test_success_default'] = 'The microphone used by default is:'; 44 | 45 | // Test network 46 | $lang['network'] = 'Network'; 47 | $lang['network_title'] = 'Test your network connection'; 48 | $lang['network_test_fail']= 'Unable to establish a WebRTC media connection to our test server.
You should be connected on media restricted network.
Please ask you local support to check your network filter rules.'; 49 | 50 | // Test room 51 | $lang['room'] = 'Room'; 52 | $lang['room_title'] = 'Test direct access to a test conference'; 53 | $lang['room_test_fail']= 'Unable to detect the connection of your echo media stream.
Please wait 30 seconds before ending the test room.
Please verify that you have check your browser permission.
Please verify your local network filter rules to our media servers. '; 54 | $lang['room_test_success']= 'Echo media Stream was connected'; 55 | 56 | // Home page 57 | $lang['home_disclaimer'] = 'This tool allows to check the following cases:'; 58 | $lang['home_browser'] = 'Browser compatibility'; 59 | $lang['home_devices'] = 'Access to your devices'; 60 | $lang['home_camera'] = 'Access to your camera video'; 61 | $lang['home_micro'] = 'Access to your microphone'; 62 | $lang['home_network'] = 'Network connections (WebRTC, TCP, UDP)'; 63 | $lang['home_room'] = 'Direct access to a conference test room'; 64 | 65 | // Network test 66 | $lang['websocket'] = 'WebSocket'; 67 | $lang['udp'] = 'UDP'; 68 | $lang['tcp'] = 'TCP'; 69 | $lang['bitrate'] = 'Bitrate:'; 70 | $lang['average_bitrate'] = 'Average:'; 71 | $lang['packetlost'] = 'Packet lost:'; 72 | $lang['framerate'] = 'Framerate:'; 73 | $lang['droppedframes'] = 'Dropped frames:'; 74 | $lang['jitter'] = 'Jitter:'; 75 | 76 | // Buttons 77 | $lang['run_all_tests'] = 'Start testing'; 78 | 79 | // Results 80 | $lang['results'] = 'Results'; 81 | $lang['results_shown_there'] = 'Results of this test will be shown here.'; 82 | $lang['all_results_shown_there'] = 'Result of tests will be shown here.'; 83 | $lang['global_test_fail'] = 'Some problems occurred while executing tests'; 84 | $lang['following_test_failed'] = 'Following test failed:'; 85 | $lang['global_more_information'] = 'For more details, you can consult the result of each test.'; 86 | 87 | // Final results 88 | $lang['global_test_success'] = 'Your work station is compatible with our service'; 89 | $lang['global_test_message'] = 'You can fully use the RendezVous service.

If you want, you can export tests result or restart the tests using the buttons below.'; -------------------------------------------------------------------------------- /src/lang/fr.php: -------------------------------------------------------------------------------- 1 | Pour bénéficier d\'une meilleure expérience utilisateur nous vous recommandons de toujours utiliser les dernières versions stables des navigateurs.'; 22 | $lang['browser_test_fail']= 'Votre navigateur n\'est pas compatible avec Jitsi Meet.
Veuillez utiliser l\'un des navigateurs suivant : Navigateurs.'; 23 | 24 | // Test devices 25 | $lang['devices'] = 'Périphériques'; 26 | $lang['devices_title'] = 'Test de l\'accès à vos périphériques'; 27 | $lang['devices_test_success'] = 'Périphériques média détéctés :'; 28 | $lang['devices_test_fail'] = 'Impossible de lister vos périphériques de capture de média (Audio et Vidéo).'; 29 | $lang['devices_audio_input_label']='Capture Audio :'; 30 | $lang['devices_audio_output_label']='Sortie Audio :'; 31 | $lang['devices_video_label']='Capture Vidéo :'; 32 | 33 | // Test camera 34 | $lang['camera'] = 'Caméra'; 35 | $lang['camera_title'] = 'Test de l\'accès à votre caméra'; 36 | $lang['camera_test_success'] = 'Votre caméra est correctement détectée.'; 37 | $lang['camera_test_success_default'] = 'La caméra utilisée par défaut est :'; 38 | 39 | // Test micro 40 | $lang['micro'] = 'Microphone'; 41 | $lang['micro_title'] = 'Test de l\'accès à votre microphone'; 42 | $lang['micro_test_success'] = 'Votre microphone est correctement détecté.'; 43 | $lang['micro_test_success_default'] = 'Le microphone utilisé par défaut est :'; 44 | 45 | // Test network 46 | $lang['network'] = 'Réseau'; 47 | $lang['network_title'] = 'Test de votre connexion réseau'; 48 | $lang['network_test_fail'] = 'Impossible de réaliser une connexion média WebRCT vers notre serveur de test.
Vous devez vous trouver dans un environnement ou la trafic WebRTC est filtré.
Veuillez vérifier auprès de vos responsable informatiques locaux les règles de filtrages des connexions'; 49 | 50 | // Test room 51 | $lang['room'] = 'Conférence'; 52 | $lang['room_title'] = 'Test de l\'accès direct à une conference de test'; 53 | $lang['room_test_fail'] = 'Impossible de detecter la connection votre flux média.
Veuillez attendre au moins 30 secondes avant d\'arrêter le test.
Veuillez vérifier vos permissions de navigateur.
Veuillez vérifier vos règles de filtrages réseaux vers nos media servers.'; 54 | $lang['room_test_success']= 'Votre flux média a bien été connecté.'; 55 | 56 | // Home page 57 | $lang['home_disclaimer'] = 'Cet outil vous permet de vérifier les cas suivant:'; 58 | $lang['home_browser'] = 'Compatibilité de votre navigateur'; 59 | $lang['home_devices'] = 'Accès à vos périphériques'; 60 | $lang['home_camera'] = 'Accès à votre caméra'; 61 | $lang['home_micro'] = 'Accès à votre microphone'; 62 | $lang['home_network'] = 'Connexions réseaux (WebRTC, TCP, UDP)'; 63 | $lang['home_room'] = 'Accès direct à une conférence de test'; 64 | 65 | // Network test 66 | $lang['websocket'] = 'WebSocket'; 67 | $lang['udp'] = 'UDP'; 68 | $lang['tcp'] = 'TCP'; 69 | $lang['bitrate'] = 'Bitrate :'; 70 | $lang['average_bitrate'] = 'Average:'; 71 | $lang['packetlost'] = 'Packetlost :'; 72 | $lang['framerate'] = 'Framerate :'; 73 | $lang['droppedframes'] = 'Dropped frames :'; 74 | $lang['jitter'] = 'Jitter :'; 75 | 76 | // Buttons 77 | $lang['run_all_tests'] = 'Démarrer le test'; 78 | 79 | // Results 80 | $lang['results'] = 'Résultats'; 81 | $lang['results_shown_there'] = 'Les résultats de ce test s\'afficheront ici.'; 82 | $lang['all_results_shown_there'] = 'Le résultat des tests s\'affichera ici.'; 83 | $lang['global_test_fail'] = 'Des problèmes sont survenus lors de l\'exécution des tests'; 84 | $lang['following_test_failed'] = 'Les tests suivant n\'ont pas été réussi :'; 85 | $lang['global_more_information'] = 'Pour plus de détails, vous pouvez consulter le résultat de chaque test.'; 86 | 87 | $lang['global_test_success'] = 'Votre poste de travail est compatible avec notre service'; 88 | $lang['global_test_message'] = 'Vous pouvez pleinement utiliser le services RendezVous.

Si vous le souhaitez, vous pouvez exporter les résultats ou relancer les tests en utilisant les boutons ci-dessous.'; 89 | -------------------------------------------------------------------------------- /src/assets/js/test_browser/ui.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TestCase: test_browser 3 | */ 4 | 5 | if (!window.hasOwnProperty('JitsiTestBrowser')) 6 | window.JitsiTestBrowser = {}; 7 | 8 | /** 9 | * Test browser case 10 | * 11 | * @type {{swapPanes: Window.JitsiTestBrowser.UI.swapPanes, showResult: Window.JitsiTestBrowser.UI.showResult}} 12 | */ 13 | window.JitsiTestBrowser.UI = { 14 | 15 | /** 16 | * Swap between right panes 17 | * 18 | * @param id ID of the pane to show 19 | * @param withMenu if true, also swap "is-active" class of left menu item 20 | */ 21 | swapPanes: function(id, withMenu = true){ 22 | // Hide all 23 | document.querySelectorAll(".test-result").forEach(function (element){ 24 | element.classList.add('hide'); 25 | }); 26 | // Show asked one 27 | const referer = document.getElementById(id).getAttribute('data-refer-to'); 28 | document.getElementById(referer).classList.remove('hide') 29 | 30 | if (withMenu){ 31 | document.querySelectorAll(".menu-item, .menu-item-small").forEach(function (element){ 32 | element.classList.remove('is-active'); 33 | }); 34 | document.querySelectorAll(`[data-pane="${id}"]`).forEach(function(element){ 35 | element.classList.add('is-active'); 36 | }); 37 | } 38 | }, 39 | 40 | 41 | /** 42 | * Update UI according to result got from test ( 43 | * 44 | * @param result 45 | * @param testCase 46 | */ 47 | showResult: function (testCase, data = {}){ 48 | if (window.JitsiTestBrowser.TestResults.hasOwnProperty(testCase)){ 49 | // Specific handling for this result 50 | window.JitsiTestBrowser.TestResults[testCase](data); 51 | 52 | }else { 53 | window.JitsiTestBrowser.TestResults.defaultRendering(testCase, data); 54 | } 55 | }, 56 | 57 | 58 | /** 59 | * Show specific loader 60 | * 61 | * @param context 62 | * @param show 63 | */ 64 | showLoader: function(context, show = true){ 65 | let container = document.querySelector(`div.result-status[data-context="${context}"] > i[data-loader]`); 66 | container.classList = ''; 67 | container.classList.add('fas', 'fa-spinner', 'fa-spin'); 68 | 69 | if (!show){ 70 | container.classList.add('hide') 71 | } 72 | }, 73 | 74 | 75 | /** 76 | * Show specific test status 77 | * 78 | * @param context 79 | * @param show 80 | */ 81 | showStatus: function(context, status = false, show = true){ 82 | let mainContainer = document.querySelector(`div.result-status[data-context="${context}"]`); 83 | mainContainer.classList.remove('hide'); 84 | 85 | let container = mainContainer.querySelector(`i[data-loader]`); 86 | container.classList = ''; 87 | container.classList.add('fa-solid', 'fa-arrow-right-long'); 88 | 89 | 90 | let sub = document.querySelector(`div.result-status[data-context="${context}"] > span`); 91 | if (show){ 92 | sub.classList.remove('hide'); 93 | }else{ 94 | sub.classList.add('hide'); 95 | } 96 | 97 | // Show status 98 | if (status !== false) { 99 | let statusContainer = sub.querySelector(`span[data-status="${status === "success" ? "OK" : "KO"}"]`); 100 | statusContainer.classList.remove('hide'); 101 | } 102 | }, 103 | 104 | /** 105 | * Blink identfied element 106 | * 107 | * @param id 108 | * @param blink 109 | */ 110 | blink: function(id, blink){ 111 | let element = document.getElementById(id); 112 | if (blink) { 113 | element.classList.add('blink'); 114 | }else{ 115 | element.classList.remove('blink'); 116 | } 117 | }, 118 | 119 | 120 | /** 121 | * Update network test status 122 | * 123 | * @param networkComponent 124 | * @param status 125 | */ 126 | updateNetworkStatus: function(networkComponent, status){ 127 | let container = document.getElementById(`media_connectivity_${networkComponent}`); 128 | let sub = container.querySelector('span[data-content="status_icon"]'); 129 | let icon = sub.querySelector('i'); 130 | 131 | container.classList.remove('test-success', 'test-fail'); 132 | sub.classList.remove('hide'); 133 | icon.classList = ''; 134 | 135 | switch (status){ 136 | case 'success': 137 | icon.classList.add('fa-solid', 'fa-check'); 138 | container.classList.add('test-success'); 139 | 140 | break; 141 | 142 | case 'fail': 143 | icon.classList.add('fa-solid', 'fa-circle-exclamation'); 144 | container.classList.add('test-fail'); 145 | break; 146 | 147 | case 'processing': 148 | icon.classList.add('fas', 'fa-spinner', 'fa-spin'); 149 | break; 150 | 151 | default: 152 | console.error(`Unknown status: ${status}`) 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "e7806260d775937d439159cde5165225", 8 | "packages": [ 9 | { 10 | "name": "matthiasmullie/minify", 11 | "version": "1.3.68", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/matthiasmullie/minify.git", 15 | "reference": "c00fb02f71b2ef0a5f53fe18c5a8b9aa30f48297" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/c00fb02f71b2ef0a5f53fe18c5a8b9aa30f48297", 20 | "reference": "c00fb02f71b2ef0a5f53fe18c5a8b9aa30f48297", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-pcre": "*", 25 | "matthiasmullie/path-converter": "~1.1", 26 | "php": ">=5.3.0" 27 | }, 28 | "require-dev": { 29 | "friendsofphp/php-cs-fixer": "~2.0", 30 | "matthiasmullie/scrapbook": "dev-master", 31 | "phpunit/phpunit": ">=4.8" 32 | }, 33 | "suggest": { 34 | "psr/cache-implementation": "Cache implementation to use with Minify::cache" 35 | }, 36 | "bin": [ 37 | "bin/minifycss", 38 | "bin/minifyjs" 39 | ], 40 | "type": "library", 41 | "autoload": { 42 | "psr-4": { 43 | "MatthiasMullie\\Minify\\": "src/" 44 | } 45 | }, 46 | "notification-url": "https://packagist.org/downloads/", 47 | "license": [ 48 | "MIT" 49 | ], 50 | "authors": [ 51 | { 52 | "name": "Matthias Mullie", 53 | "email": "minify@mullie.eu", 54 | "homepage": "http://www.mullie.eu", 55 | "role": "Developer" 56 | } 57 | ], 58 | "description": "CSS & JavaScript minifier, in PHP. Removes whitespace, strips comments, combines files (incl. @import statements and small assets in CSS files), and optimizes/shortens a few common programming patterns.", 59 | "homepage": "http://www.minifier.org", 60 | "keywords": [ 61 | "JS", 62 | "css", 63 | "javascript", 64 | "minifier", 65 | "minify" 66 | ], 67 | "funding": [ 68 | { 69 | "url": "https://github.com/matthiasmullie", 70 | "type": "github" 71 | } 72 | ], 73 | "time": "2022-04-19T08:28:56+00:00" 74 | }, 75 | { 76 | "name": "matthiasmullie/path-converter", 77 | "version": "1.1.3", 78 | "source": { 79 | "type": "git", 80 | "url": "https://github.com/matthiasmullie/path-converter.git", 81 | "reference": "e7d13b2c7e2f2268e1424aaed02085518afa02d9" 82 | }, 83 | "dist": { 84 | "type": "zip", 85 | "url": "https://api.github.com/repos/matthiasmullie/path-converter/zipball/e7d13b2c7e2f2268e1424aaed02085518afa02d9", 86 | "reference": "e7d13b2c7e2f2268e1424aaed02085518afa02d9", 87 | "shasum": "" 88 | }, 89 | "require": { 90 | "ext-pcre": "*", 91 | "php": ">=5.3.0" 92 | }, 93 | "require-dev": { 94 | "phpunit/phpunit": "~4.8" 95 | }, 96 | "type": "library", 97 | "autoload": { 98 | "psr-4": { 99 | "MatthiasMullie\\PathConverter\\": "src/" 100 | } 101 | }, 102 | "notification-url": "https://packagist.org/downloads/", 103 | "license": [ 104 | "MIT" 105 | ], 106 | "authors": [ 107 | { 108 | "name": "Matthias Mullie", 109 | "email": "pathconverter@mullie.eu", 110 | "homepage": "http://www.mullie.eu", 111 | "role": "Developer" 112 | } 113 | ], 114 | "description": "Relative path converter", 115 | "homepage": "http://github.com/matthiasmullie/path-converter", 116 | "keywords": [ 117 | "converter", 118 | "path", 119 | "paths", 120 | "relative" 121 | ], 122 | "time": "2019-02-05T23:41:09+00:00" 123 | } 124 | ], 125 | "packages-dev": [], 126 | "aliases": [], 127 | "minimum-stability": "stable", 128 | "stability-flags": [], 129 | "prefer-stable": false, 130 | "prefer-lowest": false, 131 | "platform": [], 132 | "platform-dev": [], 133 | "plugin-api-version": "1.1.0" 134 | } 135 | -------------------------------------------------------------------------------- /src/assets/js/test_browser/JitsiTestBrowser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TestCase: test_browser 3 | */ 4 | 5 | if (!window.hasOwnProperty('JitsiTestBrowser')) 6 | window.JitsiTestBrowser = {}; 7 | 8 | /** 9 | * Global test runner 10 | */ 11 | window.JitsiTestBrowser.runner = { 12 | 13 | /** 14 | * True if processing all tests (not a single one) 15 | */ 16 | all_processing: false, 17 | 18 | /** 19 | * True if force stop on failures 20 | */ 21 | stop_on_failures: false, 22 | 23 | 24 | /** 25 | * Current process status 26 | */ 27 | status: 0, 28 | 29 | 30 | /** 31 | * List of test cases 32 | */ 33 | testCases: [ 34 | 'test_browser', 'test_devices', 'test_camera', 'test_micro', 'test_network', 'test_room' 35 | ], 36 | 37 | 38 | run: function(){ 39 | this.all_processing = true; 40 | /** 41 | * Get promise to chain tests 42 | * 43 | * @param testCase 44 | * @return {Promise} 45 | */ 46 | function getPromise(testCase) { 47 | return new Promise(resolve => { 48 | // Show right panel 49 | window.JitsiTestBrowser.UI.swapPanes(testCase); 50 | 51 | // Run test 52 | window.JitsiTestBrowser[testCase].run() 53 | .then(function (data) { 54 | // Default show result 55 | window.JitsiTestBrowser.UI.showResult(testCase, data); 56 | 57 | // Add statistics 58 | if (!window.JitsiTestBrowser[testCase].hasOwnProperty('pushStatistics')) 59 | window.JitsiTestBrowser.Statistics.addStat(testCase, data); 60 | 61 | if (data.result === 'fail' && window.JitsiTestBrowser.runner.stop_on_failures){ 62 | window.JitsiTestBrowser.status = window.TestStatuses.STOPPED; 63 | } 64 | 65 | resolve(); 66 | }) 67 | .catch(function(reason){ 68 | echo(reason, testCase); 69 | }); 70 | }) 71 | } 72 | /** 73 | * Async function run tests 74 | * 75 | * @return {Promise} 76 | */ 77 | async function runTests() { 78 | const nbTestCases = window.JitsiTestBrowser.runner.testCases.length; 79 | let cpt = 1; 80 | 81 | for (const templateName of window.JitsiTestBrowser.runner.testCases) { 82 | if (window.JitsiTestBrowser.runner.stop_on_failures && window.JitsiTestBrowser.status === window.TestStatuses.STOPPED){ 83 | window.JitsiTestBrowser.runner.all_processing = false; 84 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.ENDED}); 85 | // Stop on failures 86 | return; 87 | } 88 | window.JitsiTestBrowser.UI.blink(templateName, true); 89 | await getPromise(templateName); 90 | await window.JitsiTestBrowser.runner.wait(); 91 | window.JitsiTestBrowser.UI.blink(templateName, false); 92 | 93 | // Update progress 94 | document.querySelector(".progress").style.width = `${100*cpt/nbTestCases}%`; 95 | cpt++; 96 | 97 | } 98 | 99 | // Show final results 100 | window.JitsiTestBrowser.UI.showResult('test_global'); 101 | window.JitsiTestBrowser.UI.swapPanes('test_global'); 102 | 103 | window.JitsiTestBrowser.runner.all_processing = false; 104 | 105 | // Update status 106 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.ENDED}); 107 | } 108 | 109 | runTests() 110 | }, 111 | 112 | 113 | /** 114 | * Reset 115 | */ 116 | reset: function(testCase){ 117 | // Reset stop on failures checkbox 118 | document.getElementById('stop_on_failures').removeAttribute('checked') 119 | 120 | // Show default message 121 | document.querySelectorAll('[data-result="default"]').forEach(function(element){ 122 | element.classList.remove('hide'); 123 | }); 124 | 125 | // Hide results 126 | document.querySelectorAll('[data-result="success"], [data-result="fail"]').forEach(function(element){ 127 | element.classList.add('hide'); 128 | }); 129 | document.querySelectorAll('div.result-status[data-context]').forEach(function(element){ 130 | element.classList.add('hide'); 131 | }); 132 | 133 | // Show first test case 134 | if (testCase) 135 | window.JitsiTestBrowser.UI.swapPanes(testCase); 136 | 137 | }, 138 | 139 | /** 140 | * Wait function 141 | * 142 | * @param delay Delay to wait 143 | * 144 | * @returns {Promise} 145 | */ 146 | wait: function(delay = 1000){ 147 | return new Promise(r => setTimeout(r, delay)) 148 | }, 149 | 150 | 151 | /** 152 | * Final resolve function 153 | * 154 | * @param res 155 | * @param data 156 | */ 157 | resolve: function(res, data, context){ 158 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.ENDED, "context": context, "data" : data}); 159 | res(data) 160 | } 161 | } -------------------------------------------------------------------------------- /src/assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* General */ 2 | html, body{ 3 | background: #fff; 4 | margin: 0; 5 | padding: 0; 6 | height:100%; 7 | width: 100%; 8 | font-family: Helvetica,Arial,sans-serif; 9 | color: #35476b; 10 | } 11 | 12 | body { 13 | display: flex; 14 | flex-direction: column; 15 | min-height: 100%; 16 | margin:0; 17 | } 18 | 19 | 20 | 21 | ul li{ 22 | line-height: 1.5rem; 23 | padding-left: 0.5rem; 24 | } 25 | .hide{ 26 | display: none !important; 27 | } 28 | .hidden{ 29 | visibility: hidden !important; 30 | } 31 | .error-title{ 32 | font-weight: bold; 33 | font-style: italic; 34 | font-size: large; 35 | color: red; 36 | display: block; 37 | margin:1rem; 38 | } 39 | .error-title::before{ 40 | font-family: "Font Awesome 6 Free"; 41 | content: "\f06a"; 42 | display: inline-block; 43 | padding-right: 3px; 44 | vertical-align: middle; 45 | font-weight: 900; 46 | font-style: normal; 47 | } 48 | 49 | 50 | /* Main & panels */ 51 | div#main{ 52 | flex: 1 1 auto; 53 | display: flex; 54 | flex-direction: row; 55 | } 56 | div#left-pane{ 57 | background: #5f7bbf; 58 | min-width: 5.6rem; 59 | flex: 0 1 0; 60 | display: flex; 61 | align-items: flex-start; 62 | justify-content: flex-start; 63 | flex-direction: column; 64 | } 65 | div#right-pane{ 66 | flex: 1 1 0; 67 | display:flex; 68 | flex-direction:column; 69 | padding-left:1rem; 70 | } 71 | div#right-pane h1{ 72 | font-style: italic; 73 | margin-bottom: 1rem; 74 | margin-top: 0.7rem; 75 | } 76 | div#right-pane h2{ 77 | font-style: italic; 78 | margin-left: 1rem; 79 | margin-bottom: 0.5rem; 80 | } 81 | 82 | 83 | /* Menu item */ 84 | .menu-item{ 85 | color: white; 86 | text-align: center; 87 | border: 1px solid #039be5; 88 | font-size: 2rem; 89 | cursor: pointer; 90 | height: 2rem; 91 | padding: 1rem; 92 | background: #5f7bbf; 93 | border-left: 0 solid #A8BEF3FF; 94 | transition: background 200ms ease 100ms, 95 | padding-left 100ms ease-in-out, padding-left 300ms ease-in-out, 96 | border-left 100ms ease-in-out, padding-left 300ms ease-in-out; 97 | width: 3.5rem; 98 | 99 | } 100 | .menu-item:hover{ 101 | border-bottom-left-radius: 0.5rem; 102 | border-top-left-radius: 0.5rem; 103 | background: #42a5f5; 104 | padding-left: 1rem; 105 | border-left: 0.3rem solid #A8BEF3FF; 106 | } 107 | .menu-item.is-active{ 108 | background: #42a5f5; 109 | } 110 | 111 | 112 | /* Network */ 113 | 114 | .stat-left{ 115 | display: block; 116 | float: left; 117 | width: 38%; 118 | } 119 | .stat-right{ 120 | display: block; 121 | float: left; 122 | } 123 | .stat-right:before { 124 | content: ''; 125 | display: inline-block; 126 | height: 100%; 127 | vertical-align: middle; 128 | margin-right: -0.25em; 129 | } 130 | .stat-right-item{ 131 | margin-bottom: 1rem; 132 | width: 100%; 133 | min-width: 13rem; 134 | border: 1px solid lightgrey; 135 | padding: 0.7rem; 136 | box-shadow: 2px 2px lightgrey; 137 | } 138 | .stat-right-item span[data-content="title"]{ 139 | font-weight: bold; 140 | } 141 | .stat-right-item span[data-content="status_icon"] i{ 142 | float: right; 143 | } 144 | 145 | .stat-right-item span[data-sub]{ 146 | display: block; 147 | } 148 | .stat-right-item span[data-sub="average_bitrate"]{ 149 | font-style: italic; 150 | font-size: small; 151 | font-weight: normal; 152 | } 153 | 154 | div[data-content="video_dimensions"], 155 | div[data-content="ip_connected_to"]{ 156 | font-size: smaller; 157 | } 158 | 159 | div[data-content="video_dimensions"] span[data-content="title"], 160 | div[data-content="ip_connected_to"] span[data-content="title"]{ 161 | font-weight: bold; 162 | } 163 | 164 | 165 | /* Video & room components */ 166 | video{ 167 | background-color: black; 168 | } 169 | div#video_container{ 170 | columns: 2; 171 | } 172 | #main_player { 173 | width: 80%; 174 | height: 37.5rem; 175 | } 176 | #second_player { 177 | display: none; 178 | } 179 | 180 | 181 | /* Actions */ 182 | div.actions { 183 | margin-bottom: 2rem; 184 | margin-top: 2rem; 185 | } 186 | div.actions a{ 187 | width: fit-content; 188 | margin-bottom: 1rem; 189 | padding: 1rem; 190 | border-radius: 0.5rem; 191 | color: white; 192 | cursor: pointer; 193 | background: #6c79b8; 194 | } 195 | div.actions a.disabled { 196 | background: #b4b9d0; 197 | cursor: not-allowed; 198 | } 199 | div.actions a.disabled:hover { 200 | background: #c5c7ce; 201 | } 202 | div.actions a:hover{ 203 | background: #5F7BBF; 204 | color: #eee; 205 | } 206 | 207 | /* Results */ 208 | div.test-result div a[data-action="test-runner"]{ 209 | display: block; 210 | } 211 | 212 | div.test-result div.stat-right-item.test-success{ 213 | border: 1px solid green; 214 | background-color: #ebf5d6; 215 | } 216 | div.test-result div.stat-right-item.test-success i{ 217 | color: green; 218 | } 219 | div.test-result div.stat-right-item.test-fail{ 220 | border: 1px solid orange; 221 | background-color: #fff6e9; 222 | } 223 | div.test-result div.stat-right-item.test-fail i{ 224 | color: orange; 225 | } 226 | div#network_results{ 227 | clear: both; 228 | } 229 | 230 | div.result-status{ 231 | display: inline-block; 232 | margin-left: 1rem; 233 | } 234 | span[data-status="KO"]{ 235 | color: #ff3131; 236 | } 237 | span[data-status="OK"]{ 238 | color:green; 239 | } 240 | 241 | div[data-result="success"] div[data-result="data"] div div[data-result="title"]{ 242 | font-weight: bold; 243 | margin-left: 1rem; 244 | margin-top: 1rem; 245 | } 246 | div#devices_results div[data-result="success"] div[data-result="data"] div div[data-result="content"], 247 | div#camera_results div[data-result="success"] div[data-result="data"], 248 | div#micro_results div[data-result="success"] div[data-result="data"]{ 249 | margin-left: 2rem; 250 | font-style: italic; 251 | } 252 | 253 | a#export_results{ 254 | margin-left: 1rem; 255 | } 256 | 257 | 258 | 259 | 260 | /* Run controls */ 261 | div.run-controls{ 262 | text-align: center; 263 | position: fixed; 264 | left: 50%; 265 | bottom: 20px; 266 | transform: translate(-50%, -50%); 267 | margin: 0 auto; 268 | } 269 | div.run-controls i{ 270 | border: 1px solid #35476b; 271 | padding: 0.5rem; 272 | } 273 | div.run-controls i[data-action="start"]{ 274 | border-radius: 10px 0 0 10px; 275 | } 276 | div.run-controls i[data-action="stop"]{ 277 | border-radius: 0 10px 10px 0; 278 | } 279 | 280 | div[data-context="advanced_options"]{ 281 | margin-top: 1.5rem; 282 | } 283 | div[data-context="advanced_options"] > *{ 284 | cursor: pointer; 285 | } 286 | 287 | /* media queries */ 288 | @media only screen and (max-width: 40em) { 289 | div#left-pane{ 290 | min-height: 50rem; 291 | } 292 | } 293 | 294 | 295 | /* 296 | * Media queries 297 | */ 298 | 299 | 300 | /* Small screens */ 301 | @media screen and (max-width: 39.9375em) { 302 | /* Right pane */ 303 | div#right-pane { 304 | padding-left: 0; 305 | } 306 | div#right-pane h1 { 307 | font-size: 1.4rem; 308 | margin-top: 1rem; 309 | margin-bottom: 1rem; 310 | padding-left: 1rem; 311 | } 312 | div#right-pane h2{ 313 | font-size: 1.2rem; 314 | margin-left: 0; 315 | } 316 | div.progress-container{ 317 | width: 95%; 318 | margin-left: 0.5rem; 319 | } 320 | div.test-result{ 321 | padding-left: 1rem; 322 | } 323 | 324 | 325 | /* Responsive menu */ 326 | #menu-icon-small{ 327 | position: absolute; 328 | top: 0; 329 | right: 0; 330 | margin-right: 0.5rem; 331 | 332 | padding: 1rem 0.5rem 1rem 1rem; 333 | font-size: 1.5rem; 334 | } 335 | #responsive-menu-content{ 336 | background-color: #5f7bbf; 337 | color: white; 338 | margin-bottom: 1rem; 339 | } 340 | #responsive-menu-content > div{ 341 | padding: 1rem 342 | } 343 | #responsive-menu-content div.is-active{ 344 | border-bottom-left-radius: 0.5rem; 345 | border-top-left-radius: 0.5rem; 346 | background: #42a5f5; 347 | padding-left: 1rem; 348 | border-left: 0.3rem solid #A8BEF3; 349 | } 350 | 351 | .menu-item-small{ 352 | border-top: 1px solid #039be5; 353 | 354 | padding: 0.4rem; 355 | } 356 | .menu-item-small i{ 357 | padding-right: 0.5rem; 358 | } 359 | 360 | .icon-opened:before{ 361 | font-family: "Font Awesome 6 Free"; 362 | content: "\f0c9"; 363 | display: inline-block; 364 | padding-right: 3px; 365 | vertical-align: middle; 366 | font-weight: 900; 367 | font-style: normal; 368 | 369 | } 370 | .icon-closed:before{ 371 | font-family: "Font Awesome 6 Free"; 372 | content: "\f00d"; 373 | display: inline-block; 374 | padding-right: 3px; 375 | vertical-align: middle; 376 | font-weight: 900; 377 | font-style: normal; 378 | } 379 | 380 | /* Network results */ 381 | 382 | div#network_pane .stat-left{ 383 | width:100%; 384 | } 385 | div#network_pane .stat-right{ 386 | width: 90%; 387 | } 388 | 389 | div#video_container{ 390 | columns: 1; 391 | } 392 | 393 | .hide-for-small-only { 394 | display: none !important; 395 | } 396 | } 397 | @media screen and (max-width: 0em), screen and (min-width: 40em) { 398 | .show-for-small-only { 399 | display: none !important; 400 | } 401 | } 402 | 403 | 404 | /* Medium screens */ 405 | @media only screen and (min-width: 40em) and (max-width: 73.75em) { 406 | .stat-left{ 407 | width: 63%; 408 | } 409 | 410 | div#video_container{ 411 | columns: 1; 412 | } 413 | } -------------------------------------------------------------------------------- /src/assets/js/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic function used to show test results on UI 3 | * TODO: refactor this function by anything better 4 | * 5 | * @param result 6 | * @param id 7 | */ 8 | function echo(result, id){ 9 | console.log('%o', result) 10 | let res = document.createElement('div'); 11 | res.append(JSON.stringify(result)); 12 | 13 | 14 | let referer = document.getElementById(id).getAttribute('data-refer-to'); 15 | document.getElementById(referer).append(res); 16 | } 17 | 18 | 19 | window.onload = function() { 20 | 21 | // Listen click on item (left menu) 22 | document.querySelectorAll(".menu-item, .menu-item-small").forEach(function (element){ 23 | element.addEventListener("click", function() { 24 | document.querySelectorAll(".menu-item, .menu-item-small").forEach(function (element){ 25 | element.classList.remove('is-active'); 26 | }); 27 | element.classList.add('is-active'); 28 | window.JitsiTestBrowser.UI.swapPanes(this.getAttribute('data-pane')); 29 | 30 | // Hide menu for small only 31 | document.getElementById('menu-icon-small').click(); 32 | }); 33 | }); 34 | 35 | // Listen to click on export results 36 | document.getElementById('export_results').addEventListener('click', function(){ 37 | window.JitsiTestBrowser.Statistics.export(); 38 | }); 39 | 40 | // Listen to click on re run button 41 | document.getElementById('re_run').addEventListener('click', function(){ 42 | let context = window.JitsiTestBrowser; 43 | 44 | // Reset templates 45 | for (const testCase of context.runner.testCases) { 46 | if (context.hasOwnProperty(testCase) && 47 | context[testCase].hasOwnProperty('reset')) { 48 | 49 | context[testCase].reset() 50 | 51 | } else { 52 | context.runner.reset(testCase); 53 | } 54 | } 55 | 56 | // Clear show final results 57 | context.runner.reset('results'); 58 | 59 | // Reset stats 60 | window.JitsiTestBrowser.Statistics.reset(); 61 | 62 | // Reset progress bar 63 | document.querySelector(".progress").style.width = '0%' 64 | 65 | // Restart tests 66 | window.JitsiTestBrowser.runner.run(); 67 | }); 68 | 69 | // Listen to click on run alone test case 70 | document.querySelectorAll('[data-action="test-runner"]').forEach(function (element){ 71 | element.addEventListener('click', function(){ 72 | const testCase = element.getAttribute('data-test-case'); 73 | window.JitsiTestBrowser.UI.blink(testCase, true); 74 | 75 | // Do nothing if disabled 76 | if (element.classList.contains('disabled')) return; 77 | 78 | // Reset UI and statistics first 79 | if (window.JitsiTestBrowser[testCase].hasOwnProperty('reset')){ 80 | window.JitsiTestBrowser[testCase].reset(); 81 | }else{ 82 | window.JitsiTestBrowser.runner.reset(); 83 | } 84 | window.JitsiTestBrowser.Statistics.reset(testCase); 85 | 86 | // Run 87 | window.JitsiTestBrowser[testCase].run() 88 | .then(function(result){ 89 | window.JitsiTestBrowser.UI.showResult(testCase, result); 90 | window.JitsiTestBrowser.UI.blink(testCase, false); 91 | // Change button name 92 | element.querySelector('span[data-content="title"]').innerHTML = window.lang.get('rerun_alone_test'); 93 | }) 94 | .catch(function(reason){ 95 | echo(reason, testCase) 96 | window.JitsiTestBrowser.UI.blink(testCase, false); 97 | }); 98 | }); 99 | }); 100 | 101 | // Listen to click on stop on failures 102 | document.getElementById("stop_on_failures").addEventListener('click', function(element){ 103 | window.JitsiTestBrowser.runner.stop_on_failures = this.checked === true; 104 | }); 105 | 106 | 107 | 108 | // Listen to click on run all test 109 | document.getElementById('run_all').addEventListener('click', function(){ 110 | this.setAttribute('disabled', 'disabled'); 111 | 112 | window.JitsiTestBrowser.status = window.TestStatuses.PROCESSING; 113 | 114 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.PROCESSING}); 115 | 116 | window.JitsiTestBrowser.runner.run(); 117 | }); 118 | 119 | // Listen to responsive menu button click 120 | document.getElementById('menu-icon-small').addEventListener('click', function(){ 121 | let iconOpened = this.querySelector('div span.icon-opened'); 122 | let iconClosed = this.querySelector('div span.icon-closed'); 123 | 124 | // Change icon 125 | if (this.getAttribute('data-opened') === "false"){ 126 | this.setAttribute('data-opened', "true"); 127 | iconOpened.classList.add('hide'); 128 | iconClosed.classList.remove('hide'); 129 | 130 | }else{ 131 | this.setAttribute('data-opened', "false"); 132 | iconOpened.classList.remove('hide'); 133 | iconClosed.classList.add('hide'); 134 | } 135 | 136 | // Show content 137 | document.getElementById('responsive-menu-content').classList.toggle('hide'); 138 | }); 139 | 140 | /** 141 | * Listen to "run" event 142 | * This function will temporarily disable run buttons for each test case 143 | */ 144 | document.addEventListener('run', function(element){ 145 | const status = element.status; 146 | switch (status){ 147 | case window.TestStatuses.WAITING: 148 | case window.TestStatuses.ENDED: 149 | // All buttons available if not all processing 150 | if (!window.JitsiTestBrowser.runner.all_processing) { 151 | document.querySelectorAll('[data-action="test-runner"]').forEach(function (element) { 152 | element.classList.remove('disabled'); 153 | element.removeAttribute('title'); 154 | }); 155 | } 156 | // Hide loader and show status result if needed 157 | if (element.context !== undefined){ 158 | window.JitsiTestBrowser.UI.showLoader(element.context, false); 159 | window.JitsiTestBrowser.UI.showStatus(element.context, element.data.result, true); 160 | if (element.data.result === 'fail'){ 161 | window.JitsiTestBrowser.TestResults.testsOnError.push(element.context); 162 | } 163 | } 164 | 165 | break; 166 | case window.TestStatuses.PROCESSING: 167 | // No buttons available 168 | document.querySelectorAll('[data-action="test-runner"]').forEach(function (element){ 169 | element.classList.add('disabled'); 170 | element.setAttribute('title', window.lang.get('test_running')); 171 | }); 172 | // Update loader if needed 173 | if (element.component !== undefined){ 174 | window.JitsiTestBrowser.UI.updateNetworkStatus(element.component, 'processing'); 175 | } 176 | if (element.context !== undefined){ 177 | window.JitsiTestBrowser.UI.showStatus(element.context, false, false); 178 | window.JitsiTestBrowser.UI.showLoader(element.context); 179 | } 180 | break; 181 | case window.TestStatuses.PAUSED: 182 | case window.TestStatuses.STOPPED: 183 | // TODO 184 | break; 185 | 186 | default: 187 | console.error(`Unknown status: ${status}`); 188 | } 189 | }); 190 | 191 | /** 192 | * Listen to "network_stat" event 193 | * This function will temporarily disable run buttons for each test case 194 | */ 195 | document.addEventListener('network_stat', function(element){ 196 | if (element.data !== undefined && element.context !== undefined){ 197 | switch (element.context){ 198 | case 'wss': 199 | case 'tcp': 200 | case 'udp': 201 | 202 | if (element.data.status !== undefined) { 203 | window.JitsiTestBrowser.UI.updateNetworkStatus(element.context, element.data.status); 204 | } 205 | if (element.data.framesPerSecond !== undefined){ 206 | // Got a framerate 207 | document.getElementById(`media_connectivity_framerate`) 208 | .querySelector('span[data-content="value"]').innerHTML = element.data.framesPerSecond; 209 | 210 | }else if (element.data.bitrate !== undefined){ 211 | // Got a bitrate 212 | document.getElementById(`media_connectivity_bitrate`) 213 | .querySelector('span[data-sub="bitrate"] span[data-content="value"]').innerHTML = element.data.bitrate+' kbit/s'; 214 | 215 | }else if (element.data.average_bitrate !== undefined){ 216 | // Got a bitrate 217 | document.getElementById(`media_connectivity_bitrate`) 218 | .querySelector('span[data-sub="average_bitrate"] span[data-content="value"]').innerHTML = `${element.data.average_bitrate} kbit/s`; 219 | 220 | }else if (element.data.packetLost !== undefined){ 221 | // Got droppedFrames 222 | document.getElementById(`media_connectivity_packetlost`) 223 | .querySelector('span[data-content="value"]').innerHTML = element.data.packetLost; 224 | 225 | }else if (element.data.jitter !== undefined){ 226 | // Got droppedFrames 227 | document.getElementById(`media_connectivity_jitter`) 228 | .querySelector('span[data-content="value"]').innerHTML = element.data.jitter; 229 | 230 | }else if (element.data.ip_connected_to !== undefined){ 231 | // Got droppedFrames 232 | document.querySelector(`div[data-content="ip_connected_to"]`) 233 | .querySelector('span[data-content="value"]').innerHTML = element.data.ip_connected_to; 234 | } 235 | break; 236 | 237 | 238 | case 'video_player': 239 | if (element.data.local !== undefined) { 240 | // Local video dimensions 241 | document.getElementById(`video_container`) 242 | .querySelector('section[data-content="local_stats"] div[data-content="video_dimensions"] span[data-content="value"]') 243 | .innerHTML = `${element.data.local.video_dimension.width}x${element.data.local.video_dimension.height} px`; 244 | }else{ 245 | // Remote video dimensions 246 | document.getElementById(`video_container`) 247 | .querySelector('section[data-content="remote_stats"] div[data-content="video_dimensions"] span[data-content="value"]') 248 | .innerHTML = `${element.data.remote.video_dimension.width}x${element.data.remote.video_dimension.height} px`; 249 | } 250 | break; 251 | } 252 | } 253 | }); 254 | 255 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /scripts/make-release.php: -------------------------------------------------------------------------------- 1 | Mandatory options"; 18 | echo "\t\n"; 19 | echo "\t" . ' --release : the wanted release name' . "\n"; 20 | echo "\t" . ' --debug : exec the script debugging purpose' . "\n"; 21 | echo "\t" . ' -h / --help : print this help' . "\n"; 22 | echo "\t" . ' -q : quiet mode' . "\n"; 23 | echo "\t" . ' -v : verbose mode' . "\n"; 24 | echo "\t" . ' -y : Force answer "yes" to messages prompted' . "\n"; 25 | echo "\t\n"; 26 | echo "\t> Optional options"; 27 | echo "\t\n"; 28 | echo "\t" . ' --fontawesome-kit : Font Awesome kit URL.' . "\n"; 29 | echo "\t" . ' --lang : the wanted lang code. Default is "en".' . "\n"; 30 | echo "\t" . ' --turn-endpoint : specify the turn endpoint to use for testing. Default is "https://rendez-vous.renater.fr/home/rest.php/TurnServer"' . "\n"; 31 | echo "\t" . ' --websocket-url : specify the web socket url to use for testing. Default is "wss://rendez-vous.renater.fr/colibri-ws/echo"' . "\n"; 32 | echo "\t" . ' --application-url : specify the application url to use for testing room. Default is "https://rendez-vous.renater.fr/"' . "\n"; 33 | 34 | echo "\n"; 35 | exit; 36 | } 37 | 38 | 39 | class MakeRelease{ 40 | 41 | /** 42 | * @var string Turn endpoint to use 43 | */ 44 | private static string $turnEndpoint = 'https://rendez-vous.renater.fr/home/rest.php/TurnServer'; 45 | 46 | /** 47 | * @var string Websocket URL to use 48 | */ 49 | private static string $websocketUrl = 'wss://rendez-vous.renater.fr/colibri-ws/echo'; 50 | 51 | /** 52 | * @var string Application URL to use 53 | */ 54 | private static string $applicationUrl = 'https://rendez-vous.renater.fr/'; 55 | 56 | /** 57 | * @var string Application URL to use 58 | */ 59 | private static string $fontawesomeKitURL = 'https://kit.fontawesome.com/0d01ffef9c.js'; 60 | 61 | /** 62 | * @var string Lang to use on the generated page 63 | */ 64 | private static string $langCode = 'en'; 65 | 66 | /** 67 | * @var bool Quiet mod 68 | */ 69 | public static bool $quiet = false; 70 | 71 | /** 72 | * @var bool Verbose mod 73 | */ 74 | public static bool $verbose = false; 75 | 76 | /** 77 | * @var string $release 78 | */ 79 | public static string $release = ''; 80 | 81 | 82 | /** 83 | * @var bool Force yes to prompted questions 84 | */ 85 | private static bool $forceYes = false; 86 | 87 | 88 | /** 89 | * EkkoLayer constructor. 90 | * 91 | * @param $options Array got from CLI 92 | */ 93 | public function __construct(array $options) { 94 | // Prepare quiet & verbose mod 95 | static::$quiet = array_key_exists('q', $options); 96 | static::$forceYes = array_key_exists('y', $options); 97 | static::$verbose = !static::$quiet && array_key_exists('v', $options); 98 | 99 | if (array_key_exists('release', $options)){ 100 | static::$release = $options['release']; 101 | }else{ 102 | MakeRelease::error('Parameter --release not set'); 103 | } 104 | 105 | if (array_key_exists('turn-endpoint', $options)){ 106 | static::$turnEndpoint = $options['turn-endpoint']; 107 | } 108 | if (array_key_exists('websocket-url', $options)){ 109 | static::$websocketUrl = $options['websocket-url']; 110 | } 111 | if (array_key_exists('application-url', $options)){ 112 | static::$applicationUrl = $options['application-url']; 113 | } 114 | if (array_key_exists('fontawesome-kit', $options)){ 115 | static::$fontawesomeKitURL = $options['fontawesome-kit']; 116 | } 117 | if (array_key_exists('lang', $options)){ 118 | if (!file_exists(APP_ROOT.'src/lang/'.$options['lang'].'.php')){ 119 | static::error("Lang '".$options['lang']."' not found"); 120 | }else{ 121 | static::$langCode = $options['lang']; 122 | } 123 | } 124 | } 125 | 126 | 127 | /** 128 | * @return void 129 | * @throws Minify\Exceptions\IOException 130 | */ 131 | public function process(){ 132 | $target = realpath(APP_ROOT.'/gen/').'/'.static::$release; 133 | $sources = realpath(APP_ROOT.'/src/'); 134 | 135 | $continue = true; 136 | 137 | // Create target folder 138 | static::verbose("> Create target folder [".$target."] ..."); 139 | if (is_dir($target)){ 140 | $input = static::$forceYes ? 'y' : static::prompt("[Warning] Target folder already exists. All content will be erased. Continue? [y]"); 141 | if (in_array($input, ['y', 'Y', 'yes', 'YES', ""])) { 142 | // remove old layer files 143 | if (is_dir($target)){ 144 | static::deleteDir($target); 145 | static::verbose("\t=> Folder [".$target."] deleted.\n"); 146 | } 147 | }else{ 148 | $continue = false; 149 | static::message('Operation canceled by user.'); 150 | } 151 | } 152 | 153 | if ($continue){ 154 | // Create target folder 155 | mkdir($target, 0755); 156 | 157 | // Minify sources found 158 | $minified = static::minify($sources); 159 | 160 | $cssContent = ''; 161 | $jsContent = ''; 162 | 163 | // Copy template 164 | $targetIndex = $target.'/index.html'; 165 | if (copy(APP_ROOT.'/src/index.html', $targetIndex)){ 166 | 167 | // Replace in file 168 | $str = file_get_contents($targetIndex); 169 | $str = str_replace('', $jsContent, $str); 170 | $str = str_replace('', $cssContent, $str); 171 | $str = str_replace('', static::$turnEndpoint, $str); 172 | $str = str_replace('', static::$websocketUrl, $str); 173 | $str = str_replace('', static::$applicationUrl, $str); 174 | $str = str_replace('', static::$fontawesomeKitURL, $str); 175 | 176 | // Set up lang translation 177 | $str = $this->applyTranslation($str); 178 | 179 | // Put content into index target html file 180 | file_put_contents($targetIndex, $str); 181 | 182 | static::message("File $targetIndex successfully created."); 183 | }else{ 184 | static::error('Cannot copy template file'); 185 | } 186 | } 187 | } 188 | 189 | /** 190 | * Apply asked translation 191 | * 192 | * @param $str 193 | * @return array|mixed|string|string[] 194 | */ 195 | private function applyTranslation($str){ 196 | $lang = []; 197 | 198 | include APP_ROOT.'/src/lang/'.static::$langCode.'.php'; 199 | 200 | foreach($lang as $id => $tr){ 201 | $str = str_replace('{tr:'.$id.'}', utf8_decode($tr), $str); 202 | } 203 | 204 | return str_replace('""', utf8_decode(json_encode($lang)), $str); 205 | } 206 | 207 | 208 | /** 209 | * Minify JS & CSS files 210 | * 211 | * @param $source 212 | * 213 | * @return array 214 | * 215 | * @throws Minify\Exceptions\IOException 216 | */ 217 | private static function minify($source): array { 218 | $minifierCSS = new MatthiasMullie\Minify\CSS(); 219 | $minifierJS = new MatthiasMullie\Minify\JS(); 220 | 221 | $files = static::getFiles($source); 222 | foreach ($files as $file) { 223 | $info = pathinfo($file); 224 | 225 | if (array_key_exists('extension', $info)) { 226 | if ($info['extension'] === 'css') { 227 | $minifierCSS->addFile($file); 228 | } else if ($info['extension'] === 'js') { 229 | $minifierJS->addFile($file); 230 | } 231 | } 232 | } 233 | 234 | return [ 235 | 'js' => $minifierJS->minify(), 236 | 'css' => $minifierCSS->minify() 237 | ]; 238 | } 239 | 240 | /** 241 | * Get files in dir 242 | * 243 | * @param $source 244 | * 245 | * @return array 246 | */ 247 | static function getFiles($source): array { 248 | $files = array( ); 249 | if (is_dir($source) & is_readable($source)) { 250 | $dir = dir($source); 251 | while (false !== ($file = $dir->read( ))) { 252 | // skip . and .. 253 | if (('.' === $file) || ('..' === $file) || ('debug' === $file)) { 254 | continue; 255 | } 256 | if (is_dir("$source/$file")) { 257 | $files = array_merge($files, static::getFiles("$source/$file")); 258 | } else { 259 | $files[] = "$source/$file"; 260 | } 261 | } 262 | $dir->close( ); 263 | } 264 | return $files; 265 | } 266 | 267 | 268 | 269 | /** 270 | * Prompt message and return user answer 271 | * 272 | * @param string $message 273 | * @return string 274 | */ 275 | public static function prompt(string $message = ''): string { 276 | echo $message; 277 | $handle = fopen("php://stdin", 'r'); 278 | $line = fgets($handle); 279 | return trim($line); 280 | } 281 | 282 | 283 | /** 284 | * Delete directory 285 | * 286 | * @param $dirPath 287 | */ 288 | function deleteDir($dirPath) { 289 | if (! is_dir($dirPath)) { 290 | echo "$dirPath must be a directory"; 291 | die(1); 292 | } 293 | if (substr($dirPath, strlen($dirPath) - 1, 1) != '/') { 294 | $dirPath .= '/'; 295 | } 296 | $pattern = $dirPath . '{,.}[!.,!..]*'; 297 | $files = glob($pattern, GLOB_MARK|GLOB_BRACE); 298 | foreach ($files as $file) { 299 | if (is_dir($file)) { 300 | static::deleteDir($file); 301 | } else { 302 | unlink($file); 303 | } 304 | } 305 | rmdir($dirPath); 306 | } 307 | 308 | /** 309 | * Show message in CLI 310 | * 311 | * @param $message 312 | */ 313 | public static function message($message){ 314 | if (static::$quiet) return; 315 | 316 | echo $message . "\n"; 317 | } 318 | 319 | 320 | /** 321 | * Verbose message to show in CLI 322 | * 323 | * @param $message 324 | */ 325 | public static function verbose($message){ 326 | if (!static::$verbose) return; 327 | 328 | static::message($message); 329 | } 330 | 331 | /** 332 | * Show message then die 333 | * 334 | * @param $message string Message to show 335 | */ 336 | public static function error(string $message){ 337 | die('[ERROR] ' . $message . ', exiting' . "\n"); 338 | } 339 | } 340 | 341 | // Make new release 342 | try{ 343 | $makeRelease = new MakeRelease($options); 344 | $makeRelease->process(); 345 | 346 | }catch (Exception $e){ 347 | die ("[ERROR] ".$e->getMessage()."\n"); 348 | } 349 | 350 | 351 | 352 | -------------------------------------------------------------------------------- /src/assets/js/test_browser/test_cases/test_room.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TestCase: test_room 3 | */ 4 | 5 | if (!window.hasOwnProperty('JitsiTestBrowser')) 6 | window.JitsiTestBrowser = {}; 7 | 8 | 9 | /** 10 | * Test room access case 11 | */ 12 | window.JitsiTestBrowser.test_room = { 13 | /** 14 | * Use to set player lang 15 | */ 16 | lang: { 17 | local: undefined, 18 | stored: undefined, 19 | }, 20 | 21 | /** 22 | * Interval to wait before ensuring user connection to the room 23 | */ 24 | testInterval: undefined, 25 | 26 | /** 27 | * Interval to show seconds remaining 28 | */ 29 | timerInterval: undefined, 30 | 31 | /** 32 | * Test room seconds remaining 33 | */ 34 | secondsRemaining: 30, 35 | 36 | /** 37 | * Room name 38 | */ 39 | roomName: undefined, 40 | 41 | /** 42 | * Room token 43 | */ 44 | roomToken: undefined, 45 | 46 | /** 47 | * Domain URL 48 | */ 49 | domain_url: undefined, 50 | 51 | /** 52 | * Domain 53 | */ 54 | domain: undefined, 55 | 56 | /** 57 | * Main API Client 58 | */ 59 | mainApiClient: undefined, 60 | 61 | /** 62 | * Second API client 63 | */ 64 | secondApiClient: undefined, 65 | 66 | /** 67 | * Main node player 68 | */ 69 | mainNodePlayer: undefined, 70 | 71 | /** 72 | * Second node player 73 | */ 74 | secondNodePlayer: undefined, 75 | 76 | /** 77 | * Network stats got between participants 78 | */ 79 | network_stat: null, 80 | 81 | /** 82 | * Run test 83 | * 84 | * @return {Promise<*>} 85 | */ 86 | run: function () { 87 | return new Promise(res => { 88 | console.log("> Running test_room"); 89 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.PROCESSING, "context": "test_room"}); 90 | 91 | let context = window.JitsiTestBrowser.test_room; 92 | context.status = "pending" 93 | 94 | // Init nodes 95 | context.mainNodePlayer = document.getElementById('main_player'); 96 | context.secondNodePlayer = document.getElementById('second_player'); 97 | 98 | context.mainNodePlayer.classList.remove('hide'); 99 | context.secondNodePlayer.classList.add('hide'); 100 | 101 | /** 102 | * If there is an error processing test room, display it, close connections 103 | * if needed, then resolve with "error" status 104 | * 105 | * @param reason 106 | */ 107 | let onError = function (reason) { 108 | context.onError(reason); 109 | context.closeConnections(); 110 | 111 | context.mainNodePlayer.classList.add('hide'); 112 | context.secondNodePlayer.classList.add('hide'); 113 | 114 | window.JitsiTestBrowser.runner.resolve(res, { 115 | "result": "fail", 116 | "details": reason 117 | }, "test_room"); 118 | }; 119 | 120 | // init room name & room token 121 | context.getRoomName() 122 | .then(function () { 123 | context.getRoomToken() 124 | .then(function (){ 125 | // Init test room 126 | context.initTestRoom().then(function () { 127 | // Connect clients 128 | context.connectClients().then(function () { 129 | // Add listeners to clients 130 | context.addListeners().then(function () { 131 | // Start interval 132 | context.testInterval = setTimeout(function () { 133 | // Test user connection to the room after 30 seconds 134 | context.testRoomConnection(function (result) { 135 | // Success, close connections, display results 136 | // and resolve with "success" status 137 | context.closeConnections(); 138 | context.onSuccess(result); 139 | 140 | window.JitsiTestBrowser.runner.resolve(res, {"result": result}, "test_room"); 141 | }, onError) 142 | }, 20000); 143 | 144 | // Show timer 145 | context.timerInterval = setInterval(function(){ 146 | context.secondsRemaining--; 147 | if (context.secondsRemaining < 0){ 148 | clearInterval(context.timerInterval); 149 | } 150 | 151 | }, 1000) 152 | }).catch(reason => { 153 | onError(reason); 154 | }); 155 | }).catch(reason => { 156 | onError(reason); 157 | }); 158 | }).catch(reason => { 159 | onError(reason); 160 | }); 161 | }).catch(reason => { 162 | onError(reason); 163 | }); 164 | }).catch(reason => { 165 | onError(reason); 166 | }) 167 | }) 168 | 169 | }, 170 | 171 | 172 | /** 173 | * Get room name 174 | * 175 | * @returns {Promise<*>} 176 | */ 177 | getRoomName: function(){ 178 | return new Promise((resolve, reject) => { 179 | 180 | let appUrl = document.getElementById('main').getAttribute('data-application-url'); 181 | let context = window.JitsiTestBrowser.test_room; 182 | 183 | let settings = { 184 | method: 'get', 185 | headers: new Headers({'content-type': 'text/plain'}), 186 | }; 187 | 188 | // TODO: find a way to make it work without this call 189 | fetch(`${appUrl}/home/rest.php/TestBrowser/RoomName`, settings) 190 | .then(response => { 191 | response.text() 192 | .then(function (data) { 193 | context.roomName = data; 194 | resolve(); 195 | }) 196 | .catch(reason => { 197 | reject({"status": "fail", "error": reason.toString()}); 198 | }); 199 | } 200 | ) 201 | .catch(reason => { 202 | reject({"status": "fail", "error": reason.toString()}); 203 | }); 204 | }); 205 | }, 206 | 207 | 208 | /** 209 | * Get room token 210 | * 211 | * @returns {Promise<*>} 212 | */ 213 | getRoomToken: function() { 214 | return new Promise((resolve, reject) => { 215 | let appUrl = document.getElementById('main').getAttribute('data-application-url'); 216 | let context = window.JitsiTestBrowser.test_room; 217 | 218 | let settings = { 219 | method: 'get' 220 | }; 221 | 222 | // TODO: find a way to make it work without this call 223 | fetch(`${appUrl}/home/rest.php/TestBrowser/RoomToken?` + new URLSearchParams( 224 | { 225 | RoomName: context.roomName 226 | } 227 | ), settings) 228 | .then(response => { 229 | response.text() 230 | .then(function (data) { 231 | context.roomToken = decodeURIComponent(data); 232 | resolve(); 233 | }) 234 | .catch(reason => { 235 | reject({"status": "fail", "error": reason.toString()}); 236 | }); 237 | } 238 | ) 239 | }); 240 | }, 241 | 242 | /** 243 | * Add listener for api client. 244 | * 245 | * Listeners used: 246 | * participantJoined: when a participant join the call 247 | * videoConferenceLeft: when participants left the call 248 | * 249 | * @return {Promise} 250 | */ 251 | addListeners: function () { 252 | return new Promise((resolve) => { 253 | let context = window.JitsiTestBrowser.test_room; 254 | 255 | context.mainApiClient.addEventListener("networkStatUpdated", function (data) { 256 | console.log('[networkStatUpdated]'); 257 | console.log(data); 258 | context.network_stat = data 259 | }); 260 | context.mainApiClient.addEventListener("participantJoined", function () { 261 | let context = window.JitsiTestBrowser.test_room; 262 | context.mainApiClient.removeEventListener("participantJoined"); 263 | }); 264 | 265 | context.mainApiClient.addEventListener("videoConferenceLeft", function () { 266 | context.mainApiClient.removeEventListener("videoConferenceLeft"); 267 | 268 | // Close connections on participant left 269 | // TODO: is it usefull anymore? 270 | context.closeConnections(); 271 | }); 272 | 273 | resolve(); 274 | }); 275 | }, 276 | 277 | 278 | /** 279 | * Init test room 280 | * 281 | * @return {Promise} 282 | */ 283 | initTestRoom: function () { 284 | return new Promise((resolve, reject) => { 285 | console.log('[test_room]: Init test room...'); 286 | 287 | let context = window.JitsiTestBrowser.test_room; 288 | 289 | // Set lang into local storage to force translation into jitsi test room 290 | context.lang.local = document.querySelector('html').getAttribute('lang'); 291 | context.lang.stored = localStorage.getItem('language'); 292 | localStorage.removeItem('language'); 293 | localStorage.setItem('language', context.lang.local); 294 | 295 | if (context.mainApiClient) 296 | context.mainApiClient.dispose(); 297 | if (context.secondApiClient) 298 | context.secondApiClient.dispose(); 299 | 300 | // Init domain 301 | context.domain = document.getElementById('main').getAttribute('data-application-url'); 302 | context.domain_url = context.domain +'/home'; 303 | 304 | /** 305 | * Get session ID using the Room endpoint 306 | */ 307 | let appUrl = context.domain_url; 308 | 309 | let settings = { 310 | method: 'get' 311 | }; 312 | 313 | // TODO: find a way to make it work without this call 314 | fetch(`${appUrl}/rest.php/Room?`+ new URLSearchParams( 315 | { 316 | roomName: context.roomName, 317 | domain: context.domain, 318 | token: context.roomToken, 319 | } 320 | ), settings) 321 | .then(response => { 322 | response.json() 323 | .then(function (data) { 324 | console.log('[test_room]: OK'); 325 | if (data.status === 'success') { 326 | console.log('[test_room]: Test room initialized.'); 327 | resolve(); 328 | } else { 329 | reject(data); 330 | } 331 | }) 332 | .catch(reason => { 333 | reject({"status": "fail", "error": reason.toString()}); 334 | }); 335 | } 336 | ) 337 | }); 338 | }, 339 | 340 | 341 | /** 342 | * Connect API clients to the call 343 | * 344 | * @return {Promise} 345 | */ 346 | connectClients: function () { 347 | return new Promise((resolve) => { 348 | let context = window.JitsiTestBrowser.test_room; 349 | console.log('[test_room]: Connect clients...'); 350 | 351 | let mainOptions = { 352 | roomName: context.roomName, 353 | width: context.mainNodePlayer.width, 354 | height: context.mainNodePlayer.height, 355 | interfaceConfigOverwrite: { 356 | CLOSE_PAGE_GUEST_HINT: true 357 | }, 358 | configOverwrite: { 359 | callStatsID: '', 360 | defaultLanguage: context.lang.local, 361 | enablePopupExternalAuth: true, 362 | startWithAudioMuted: true, 363 | startWithVideoMuted: true, 364 | p2p: {enabled: false}, 365 | desktopSharingChromeDisabled: true 366 | }, 367 | parentNode: context.mainNodePlayer 368 | } 369 | 370 | let secondOptions = { 371 | roomName: context.roomName, 372 | width: context.secondNodePlayer.width, 373 | height: context.secondNodePlayer.height, 374 | interfaceConfigOverwrite: { 375 | TOOLBAR_BUTTONS: ['microphone', 'camera', 'settings', 'hangup'], 376 | MAIN_TOOLBAR_BUTTONS: [], 377 | INITIAL_TOOLBAR_TIMEOUT: 0 378 | }, 379 | configOverwrite: { 380 | callStatsID: '', 381 | enablePopupExternalAuth: true, 382 | p2p: {enabled: false} 383 | }, 384 | parentNode: context.secondNodePlayer 385 | } 386 | 387 | // Connect main client 388 | let subDomain = context.domain.replace(/^https?:\/\//, ''); 389 | context.mainApiClient = new JitsiMeetExternalAPI(subDomain, mainOptions); 390 | 391 | // Tricks to get media, connect another (same) client one second later 392 | setTimeout(function () { 393 | context.secondApiClient = new JitsiMeetExternalAPI(subDomain, secondOptions); 394 | console.log('[test_room]: Clients connected.'); 395 | resolve(); 396 | }, 1000); 397 | 398 | 399 | }); 400 | }, 401 | 402 | /** 403 | * Test user connection on the room 404 | * 405 | * @param resolve 406 | * @param reject 407 | */ 408 | testRoomConnection: function (resolve, reject) { 409 | console.log('[test_room]: test room connection ...'); 410 | let context = window.JitsiTestBrowser.test_room; 411 | 412 | context.mainApiClient.getNetworkStat(); 413 | 414 | setTimeout(function(){ 415 | if (context.network_stat === null || !context.network_stat.hasOwnProperty('stat')){ 416 | reject({"status": "fail", "reason": "network_error"}) 417 | } 418 | 419 | // Check if there is participants 420 | if (context.secondApiClient.getParticipantsInfo().length !== 2) { 421 | // not participant 422 | reject({"status": "fail", "reason": "no_participant"}) 423 | } else { 424 | resolve("success"); 425 | } 426 | }, 2000) 427 | }, 428 | 429 | /** 430 | * Close API connections, clear test interval 431 | */ 432 | closeConnections: function () { 433 | let context = window.JitsiTestBrowser.test_room; 434 | console.log('[test_room]: Close connections ...'); 435 | 436 | // Close connections for main & second client 437 | if (context.mainApiClient) { 438 | context.mainApiClient.executeCommand('hangup'); 439 | context.secondApiClient.executeCommand('hangup'); 440 | context.mainApiClient.removeEventListener("videoConferenceLeft"); 441 | context.mainApiClient.removeEventListener("participantJoined"); 442 | setTimeout(function () { 443 | if (context.mainApiClient){ 444 | context.mainApiClient.dispose(); 445 | } 446 | if (context.secondApiClient) { 447 | context.secondApiClient.dispose(); 448 | } 449 | context.mainApiClient = undefined; 450 | context.secondApiClient = undefined; 451 | }, 1000); 452 | localStorage.setItem('language', context.lang.stored); 453 | } 454 | // Remove timeouts 455 | if (context.testInterval) { 456 | clearInterval(context.testInterval); 457 | context.testInterval = undefined; 458 | } 459 | 460 | console.log('[test_room]: Connections closed.'); 461 | }, 462 | 463 | 464 | /** 465 | * Function to show test errors 466 | * 467 | * @param error 468 | */ 469 | onError: function (error) { 470 | let context = window.JitsiTestBrowser.test_room; 471 | 472 | console.log('[test_room]: Error'); 473 | 474 | context.statuses = "error"; 475 | 476 | let details = error; 477 | if (error instanceof Error) { 478 | details = error.toString(); 479 | 480 | } else if (error instanceof Object) { 481 | details = JSON.stringify(error); 482 | } 483 | console.log(details); 484 | }, 485 | 486 | /** 487 | * Function to show test success 488 | * 489 | * @param result 490 | */ 491 | onSuccess: function (result) { 492 | console.log('[test_room]: All test successful'); 493 | console.log(result); 494 | 495 | // hide room player 496 | window.JitsiTestBrowser.test_room.mainNodePlayer.classList.add('hide'); 497 | } 498 | } -------------------------------------------------------------------------------- /src/assets/js/test_browser/utils/bowser.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bowser - a browser detector 3 | * https://github.com/ded/bowser 4 | * MIT License | (c) Dustin Diaz 2015 5 | */ 6 | 7 | !function (root, name, definition) { 8 | if (typeof module != 'undefined' && module.exports) module.exports = definition() 9 | else if (typeof define == 'function' && define.amd) define(name, definition) 10 | else root[name] = definition() 11 | }(this, 'bowser', function () { 12 | /** 13 | * See useragents.js for examples of navigator.userAgent 14 | */ 15 | 16 | var t = true 17 | 18 | function detect(ua) { 19 | 20 | function getFirstMatch(regex) { 21 | var match = ua.match(regex); 22 | return (match && match.length > 1 && match[1]) || ''; 23 | } 24 | 25 | function getSecondMatch(regex) { 26 | var match = ua.match(regex); 27 | return (match && match.length > 1 && match[2]) || ''; 28 | } 29 | 30 | var iosdevice = getFirstMatch(/(ipod|iphone|ipad)/i).toLowerCase() 31 | , likeAndroid = /like android/i.test(ua) 32 | , android = !likeAndroid && /android/i.test(ua) 33 | , nexusMobile = /nexus\s*[0-6]\s*/i.test(ua) 34 | , nexusTablet = !nexusMobile && /nexus\s*[0-9]+/i.test(ua) 35 | , chromeos = /CrOS/.test(ua) 36 | , silk = /silk/i.test(ua) 37 | , sailfish = /sailfish/i.test(ua) 38 | , tizen = /tizen/i.test(ua) 39 | , webos = /(web|hpw)os/i.test(ua) 40 | , windowsphone = /windows phone/i.test(ua) 41 | , samsungBrowser = /SamsungBrowser/i.test(ua) 42 | , windows = !windowsphone && /windows/i.test(ua) 43 | , mac = !iosdevice && !silk && /macintosh/i.test(ua) 44 | , linux = !android && !sailfish && !tizen && !webos && /linux/i.test(ua) 45 | , edgeVersion = getSecondMatch(/edg([ea]|ios)\/(\d+(\.\d+)?)/i) 46 | , versionIdentifier = getFirstMatch(/version\/(\d+(\.\d+)?)/i) 47 | , tablet = /tablet/i.test(ua) && !/tablet pc/i.test(ua) 48 | , mobile = !tablet && /[^-]mobi/i.test(ua) 49 | , xbox = /xbox/i.test(ua) 50 | , result 51 | 52 | if (/opera/i.test(ua)) { 53 | // an old Opera 54 | result = { 55 | name: 'Opera' 56 | , opera: t 57 | , version: versionIdentifier || getFirstMatch(/(?:opera|opr|opios)[\s\/](\d+(\.\d+)?)/i) 58 | } 59 | } else if (/opr\/|opios/i.test(ua)) { 60 | // a new Opera 61 | result = { 62 | name: 'Opera' 63 | , opera: t 64 | , version: getFirstMatch(/(?:opr|opios)[\s\/](\d+(\.\d+)?)/i) || versionIdentifier 65 | } 66 | } 67 | else if (/SamsungBrowser/i.test(ua)) { 68 | result = { 69 | name: 'Samsung Internet for Android' 70 | , samsungBrowser: t 71 | , version: versionIdentifier || getFirstMatch(/(?:SamsungBrowser)[\s\/](\d+(\.\d+)?)/i) 72 | } 73 | } 74 | else if (/coast/i.test(ua)) { 75 | result = { 76 | name: 'Opera Coast' 77 | , coast: t 78 | , version: versionIdentifier || getFirstMatch(/(?:coast)[\s\/](\d+(\.\d+)?)/i) 79 | } 80 | } 81 | else if (/yabrowser/i.test(ua)) { 82 | result = { 83 | name: 'Yandex Browser' 84 | , yandexbrowser: t 85 | , version: versionIdentifier || getFirstMatch(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i) 86 | } 87 | } 88 | else if (/ucbrowser/i.test(ua)) { 89 | result = { 90 | name: 'UC Browser' 91 | , ucbrowser: t 92 | , version: getFirstMatch(/(?:ucbrowser)[\s\/](\d+(?:\.\d+)+)/i) 93 | } 94 | } 95 | else if (/mxios/i.test(ua)) { 96 | result = { 97 | name: 'Maxthon' 98 | , maxthon: t 99 | , version: getFirstMatch(/(?:mxios)[\s\/](\d+(?:\.\d+)+)/i) 100 | } 101 | } 102 | else if (/epiphany/i.test(ua)) { 103 | result = { 104 | name: 'Epiphany' 105 | , epiphany: t 106 | , version: getFirstMatch(/(?:epiphany)[\s\/](\d+(?:\.\d+)+)/i) 107 | } 108 | } 109 | else if (/puffin/i.test(ua)) { 110 | result = { 111 | name: 'Puffin' 112 | , puffin: t 113 | , version: getFirstMatch(/(?:puffin)[\s\/](\d+(?:\.\d+)?)/i) 114 | } 115 | } 116 | else if (/sleipnir/i.test(ua)) { 117 | result = { 118 | name: 'Sleipnir' 119 | , sleipnir: t 120 | , version: getFirstMatch(/(?:sleipnir)[\s\/](\d+(?:\.\d+)+)/i) 121 | } 122 | } 123 | else if (/k-meleon/i.test(ua)) { 124 | result = { 125 | name: 'K-Meleon' 126 | , kMeleon: t 127 | , version: getFirstMatch(/(?:k-meleon)[\s\/](\d+(?:\.\d+)+)/i) 128 | } 129 | } 130 | else if (windowsphone) { 131 | result = { 132 | name: 'Windows Phone' 133 | , osname: 'Windows Phone' 134 | , windowsphone: t 135 | } 136 | if (edgeVersion) { 137 | result.msedge = t 138 | result.version = edgeVersion 139 | } 140 | else { 141 | result.msie = t 142 | result.version = getFirstMatch(/iemobile\/(\d+(\.\d+)?)/i) 143 | } 144 | } 145 | else if (/msie|trident/i.test(ua)) { 146 | result = { 147 | name: 'Internet Explorer' 148 | , msie: t 149 | , version: getFirstMatch(/(?:msie |rv:)(\d+(\.\d+)?)/i) 150 | } 151 | } else if (chromeos) { 152 | result = { 153 | name: 'Chrome' 154 | , osname: 'Chrome OS' 155 | , chromeos: t 156 | , chromeBook: t 157 | , chrome: t 158 | , version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i) 159 | } 160 | } else if (/edg([ea]|ios)/i.test(ua)) { 161 | result = { 162 | name: 'Microsoft Edge' 163 | , msedge: t 164 | , version: edgeVersion 165 | } 166 | } 167 | else if (/vivaldi/i.test(ua)) { 168 | result = { 169 | name: 'Vivaldi' 170 | , vivaldi: t 171 | , version: getFirstMatch(/vivaldi\/(\d+(\.\d+)?)/i) || versionIdentifier 172 | } 173 | } 174 | else if (sailfish) { 175 | result = { 176 | name: 'Sailfish' 177 | , osname: 'Sailfish OS' 178 | , sailfish: t 179 | , version: getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i) 180 | } 181 | } 182 | else if (/seamonkey\//i.test(ua)) { 183 | result = { 184 | name: 'SeaMonkey' 185 | , seamonkey: t 186 | , version: getFirstMatch(/seamonkey\/(\d+(\.\d+)?)/i) 187 | } 188 | } 189 | else if (/firefox|iceweasel|fxios/i.test(ua)) { 190 | result = { 191 | name: 'Firefox' 192 | , firefox: t 193 | , version: getFirstMatch(/(?:firefox|iceweasel|fxios)[ \/](\d+(\.\d+)?)/i) 194 | } 195 | if (/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(ua)) { 196 | result.firefoxos = t 197 | result.osname = 'Firefox OS' 198 | } 199 | } 200 | else if (silk) { 201 | result = { 202 | name: 'Amazon Silk' 203 | , silk: t 204 | , version : getFirstMatch(/silk\/(\d+(\.\d+)?)/i) 205 | } 206 | } 207 | else if (/phantom/i.test(ua)) { 208 | result = { 209 | name: 'PhantomJS' 210 | , phantom: t 211 | , version: getFirstMatch(/phantomjs\/(\d+(\.\d+)?)/i) 212 | } 213 | } 214 | else if (/slimerjs/i.test(ua)) { 215 | result = { 216 | name: 'SlimerJS' 217 | , slimer: t 218 | , version: getFirstMatch(/slimerjs\/(\d+(\.\d+)?)/i) 219 | } 220 | } 221 | else if (/blackberry|\bbb\d+/i.test(ua) || /rim\stablet/i.test(ua)) { 222 | result = { 223 | name: 'BlackBerry' 224 | , osname: 'BlackBerry OS' 225 | , blackberry: t 226 | , version: versionIdentifier || getFirstMatch(/blackberry[\d]+\/(\d+(\.\d+)?)/i) 227 | } 228 | } 229 | else if (webos) { 230 | result = { 231 | name: 'WebOS' 232 | , osname: 'WebOS' 233 | , webos: t 234 | , version: versionIdentifier || getFirstMatch(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i) 235 | }; 236 | /touchpad\//i.test(ua) && (result.touchpad = t) 237 | } 238 | else if (/bada/i.test(ua)) { 239 | result = { 240 | name: 'Bada' 241 | , osname: 'Bada' 242 | , bada: t 243 | , version: getFirstMatch(/dolfin\/(\d+(\.\d+)?)/i) 244 | }; 245 | } 246 | else if (tizen) { 247 | result = { 248 | name: 'Tizen' 249 | , osname: 'Tizen' 250 | , tizen: t 251 | , version: getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i) || versionIdentifier 252 | }; 253 | } 254 | else if (/qupzilla/i.test(ua)) { 255 | result = { 256 | name: 'QupZilla' 257 | , qupzilla: t 258 | , version: getFirstMatch(/(?:qupzilla)[\s\/](\d+(?:\.\d+)+)/i) || versionIdentifier 259 | } 260 | } 261 | else if (/chromium/i.test(ua)) { 262 | result = { 263 | name: 'Chromium' 264 | , chromium: t 265 | , version: getFirstMatch(/(?:chromium)[\s\/](\d+(?:\.\d+)?)/i) || versionIdentifier 266 | } 267 | } 268 | else if (/chrome|crios|crmo/i.test(ua)) { 269 | result = { 270 | name: 'Chrome' 271 | , chrome: t 272 | , version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i) 273 | } 274 | } 275 | else if (android) { 276 | result = { 277 | name: 'Android' 278 | , version: versionIdentifier 279 | } 280 | } 281 | else if (/safari|applewebkit/i.test(ua)) { 282 | result = { 283 | name: 'Safari' 284 | , safari: t 285 | } 286 | if (versionIdentifier) { 287 | result.version = versionIdentifier 288 | } 289 | } 290 | else if (iosdevice) { 291 | result = { 292 | name : iosdevice == 'iphone' ? 'iPhone' : iosdevice == 'ipad' ? 'iPad' : 'iPod' 293 | } 294 | // WTF: version is not part of user agent in web apps 295 | if (versionIdentifier) { 296 | result.version = versionIdentifier 297 | } 298 | } 299 | else if(/googlebot/i.test(ua)) { 300 | result = { 301 | name: 'Googlebot' 302 | , googlebot: t 303 | , version: getFirstMatch(/googlebot\/(\d+(\.\d+))/i) || versionIdentifier 304 | } 305 | } 306 | else { 307 | result = { 308 | name: getFirstMatch(/^(.*)\/(.*) /), 309 | version: getSecondMatch(/^(.*)\/(.*) /) 310 | }; 311 | } 312 | 313 | // set webkit or gecko flag for browsers based on these engines 314 | if (!result.msedge && /(apple)?webkit/i.test(ua)) { 315 | if (/(apple)?webkit\/537\.36/i.test(ua)) { 316 | result.name = result.name || "Blink" 317 | result.blink = t 318 | } else { 319 | result.name = result.name || "Webkit" 320 | result.webkit = t 321 | } 322 | if (!result.version && versionIdentifier) { 323 | result.version = versionIdentifier 324 | } 325 | } else if (!result.opera && /gecko\//i.test(ua)) { 326 | result.name = result.name || "Gecko" 327 | result.gecko = t 328 | result.version = result.version || getFirstMatch(/gecko\/(\d+(\.\d+)?)/i) 329 | } 330 | 331 | // set OS flags for platforms that have multiple browsers 332 | if (!result.windowsphone && (android || result.silk)) { 333 | result.android = t 334 | result.osname = 'Android' 335 | } else if (!result.windowsphone && iosdevice) { 336 | result[iosdevice] = t 337 | result.ios = t 338 | result.osname = 'iOS' 339 | } else if (mac) { 340 | result.mac = t 341 | result.osname = 'macOS' 342 | } else if (xbox) { 343 | result.xbox = t 344 | result.osname = 'Xbox' 345 | } else if (windows) { 346 | result.windows = t 347 | result.osname = 'Windows' 348 | } else if (linux) { 349 | result.linux = t 350 | result.osname = 'Linux' 351 | } 352 | 353 | function getWindowsVersion (s) { 354 | switch (s) { 355 | case 'NT': return 'NT' 356 | case 'XP': return 'XP' 357 | case 'NT 5.0': return '2000' 358 | case 'NT 5.1': return 'XP' 359 | case 'NT 5.2': return '2003' 360 | case 'NT 6.0': return 'Vista' 361 | case 'NT 6.1': return '7' 362 | case 'NT 6.2': return '8' 363 | case 'NT 6.3': return '8.1' 364 | case 'NT 10.0': return '10' 365 | default: return undefined 366 | } 367 | } 368 | 369 | // OS version extraction 370 | var osVersion = ''; 371 | if (result.windows) { 372 | osVersion = getWindowsVersion(getFirstMatch(/Windows ((NT|XP)( \d\d?.\d)?)/i)) 373 | } else if (result.windowsphone) { 374 | osVersion = getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i); 375 | } else if (result.mac) { 376 | osVersion = getFirstMatch(/Mac OS X (\d+([_\.\s]\d+)*)/i); 377 | osVersion = osVersion.replace(/[_\s]/g, '.'); 378 | } else if (iosdevice) { 379 | osVersion = getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i); 380 | osVersion = osVersion.replace(/[_\s]/g, '.'); 381 | } else if (android) { 382 | osVersion = getFirstMatch(/android[ \/-](\d+(\.\d+)*)/i); 383 | } else if (result.webos) { 384 | osVersion = getFirstMatch(/(?:web|hpw)os\/(\d+(\.\d+)*)/i); 385 | } else if (result.blackberry) { 386 | osVersion = getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i); 387 | } else if (result.bada) { 388 | osVersion = getFirstMatch(/bada\/(\d+(\.\d+)*)/i); 389 | } else if (result.tizen) { 390 | osVersion = getFirstMatch(/tizen[\/\s](\d+(\.\d+)*)/i); 391 | } 392 | if (osVersion) { 393 | result.osversion = osVersion; 394 | } 395 | else { 396 | osVersion = ''; 397 | } 398 | 399 | // device type extraction 400 | var osMajorVersion = !result.windows && osVersion.split('.')[0]; 401 | if ( 402 | tablet 403 | || nexusTablet 404 | || iosdevice == 'ipad' 405 | || (android && (osMajorVersion == 3 || (osMajorVersion >= 4 && !mobile))) 406 | || result.silk 407 | ) { 408 | result.tablet = t 409 | } else if ( 410 | mobile 411 | || iosdevice == 'iphone' 412 | || iosdevice == 'ipod' 413 | || android 414 | || nexusMobile 415 | || result.blackberry 416 | || result.webos 417 | || result.bada 418 | ) { 419 | result.mobile = t 420 | } 421 | 422 | // Graded Browser Support 423 | // http://developer.yahoo.com/yui/articles/gbs 424 | if (result.msedge || 425 | (result.msie && result.version >= 10) || 426 | (result.yandexbrowser && result.version >= 15) || 427 | (result.vivaldi && result.version >= 1.0) || 428 | (result.chrome && result.version >= 20) || 429 | (result.samsungBrowser && result.version >= 4) || 430 | (result.firefox && result.version >= 20.0) || 431 | (result.safari && result.version >= 6) || 432 | (result.opera && result.version >= 10.0) || 433 | (result.ios && result.osversion && result.osversion.split(".")[0] >= 6) || 434 | (result.blackberry && result.version >= 10.1) 435 | || (result.chromium && result.version >= 20) 436 | ) { 437 | result.a = t; 438 | } 439 | else if ((result.msie && result.version < 10) || 440 | (result.chrome && result.version < 20) || 441 | (result.firefox && result.version < 20.0) || 442 | (result.safari && result.version < 6) || 443 | (result.opera && result.version < 10.0) || 444 | (result.ios && result.osversion && result.osversion.split(".")[0] < 6) 445 | || (result.chromium && result.version < 20) 446 | ) { 447 | result.c = t 448 | } else result.x = t 449 | 450 | return result 451 | } 452 | 453 | var bowser = detect(typeof navigator !== 'undefined' ? navigator.userAgent || '' : '') 454 | 455 | bowser.test = function (browserList) { 456 | for (var i = 0; i < browserList.length; ++i) { 457 | var browserItem = browserList[i]; 458 | if (typeof browserItem=== 'string') { 459 | if (browserItem in bowser) { 460 | return true; 461 | } 462 | } 463 | } 464 | return false; 465 | } 466 | 467 | /** 468 | * Get version precisions count 469 | * 470 | * @example 471 | * getVersionPrecision("1.10.3") // 3 472 | * 473 | * @param {string} version 474 | * @return {number} 475 | */ 476 | function getVersionPrecision(version) { 477 | return version.split(".").length; 478 | } 479 | 480 | /** 481 | * Array::map polyfill 482 | * 483 | * @param {Array} arr 484 | * @param {Function} iterator 485 | * @return {Array} 486 | */ 487 | function map(arr, iterator) { 488 | var result = [], i; 489 | if (Array.prototype.map) { 490 | return Array.prototype.map.call(arr, iterator); 491 | } 492 | for (i = 0; i < arr.length; i++) { 493 | result.push(iterator(arr[i])); 494 | } 495 | return result; 496 | } 497 | 498 | /** 499 | * Calculate browser version weight 500 | * 501 | * @example 502 | * compareVersions(['1.10.2.1', '1.8.2.1.90']) // 1 503 | * compareVersions(['1.010.2.1', '1.09.2.1.90']); // 1 504 | * compareVersions(['1.10.2.1', '1.10.2.1']); // 0 505 | * compareVersions(['1.10.2.1', '1.0800.2']); // -1 506 | * 507 | * @param {Array} versions versions to compare 508 | * @return {Number} comparison result 509 | */ 510 | function compareVersions(versions) { 511 | // 1) get common precision for both versions, for example for "10.0" and "9" it should be 2 512 | var precision = Math.max(getVersionPrecision(versions[0]), getVersionPrecision(versions[1])); 513 | var chunks = map(versions, function (version) { 514 | var delta = precision - getVersionPrecision(version); 515 | 516 | // 2) "9" -> "9.0" (for precision = 2) 517 | version = version + new Array(delta + 1).join(".0"); 518 | 519 | // 3) "9.0" -> ["000000000"", "000000009"] 520 | return map(version.split("."), function (chunk) { 521 | return new Array(20 - chunk.length).join("0") + chunk; 522 | }).reverse(); 523 | }); 524 | 525 | // iterate in reverse order by reversed chunks array 526 | while (--precision >= 0) { 527 | // 4) compare: "000000009" > "000000010" = false (but "9" > "10" = true) 528 | if (chunks[0][precision] > chunks[1][precision]) { 529 | return 1; 530 | } 531 | else if (chunks[0][precision] === chunks[1][precision]) { 532 | if (precision === 0) { 533 | // all version chunks are same 534 | return 0; 535 | } 536 | } 537 | else { 538 | return -1; 539 | } 540 | } 541 | } 542 | 543 | /** 544 | * Check if browser is unsupported 545 | * 546 | * @example 547 | * bowser.isUnsupportedBrowser({ 548 | * msie: "10", 549 | * firefox: "23", 550 | * chrome: "29", 551 | * safari: "5.1", 552 | * opera: "16", 553 | * phantom: "534" 554 | * }); 555 | * 556 | * @param {Object} minVersions map of minimal version to browser 557 | * @param {Boolean} [strictMode = false] flag to return false if browser wasn't found in map 558 | * @param {String} [ua] user agent string 559 | * @return {Boolean} 560 | */ 561 | function isUnsupportedBrowser(minVersions, strictMode, ua) { 562 | var _bowser = bowser; 563 | 564 | // make strictMode param optional with ua param usage 565 | if (typeof strictMode === 'string') { 566 | ua = strictMode; 567 | strictMode = void(0); 568 | } 569 | 570 | if (strictMode === void(0)) { 571 | strictMode = false; 572 | } 573 | if (ua) { 574 | _bowser = detect(ua); 575 | } 576 | 577 | var version = "" + _bowser.version; 578 | for (var browser in minVersions) { 579 | if (minVersions.hasOwnProperty(browser)) { 580 | if (_bowser[browser]) { 581 | if (typeof minVersions[browser] !== 'string') { 582 | throw new Error('Browser version in the minVersion map should be a string: ' + browser + ': ' + String(minVersions)); 583 | } 584 | 585 | // browser version and min supported version. 586 | return compareVersions([version, minVersions[browser]]) < 0; 587 | } 588 | } 589 | } 590 | 591 | return strictMode; // not found 592 | } 593 | 594 | /** 595 | * Check if browser is supported 596 | * 597 | * @param {Object} minVersions map of minimal version to browser 598 | * @param {Boolean} [strictMode = false] flag to return false if browser wasn't found in map 599 | * @param {String} [ua] user agent string 600 | * @return {Boolean} 601 | */ 602 | function check(minVersions, strictMode, ua) { 603 | return !isUnsupportedBrowser(minVersions, strictMode, ua); 604 | } 605 | 606 | bowser.isUnsupportedBrowser = isUnsupportedBrowser; 607 | bowser.compareVersions = compareVersions; 608 | bowser.check = check; 609 | 610 | /* 611 | * Set our detect method to the main bowser object so we can 612 | * reuse it to test other user agents. 613 | * This is needed to implement future tests. 614 | */ 615 | bowser._detect = detect; 616 | 617 | /* 618 | * Set our detect public method to the main bowser object 619 | * This is needed to implement bowser in server side 620 | */ 621 | bowser.detect = detect; 622 | return bowser 623 | }); 624 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
20 | 21 |
22 | 25 | 28 | 29 | 32 | 33 | 36 | 37 | 40 | 41 | 44 | 45 | 48 | 49 | 52 |
53 | 54 | 55 |
56 | 57 | 58 | 65 | 66 |

{tr:main_title}

67 | 68 | 69 |
70 | 73 | 76 | 77 | 80 | 81 | 84 | 85 | 88 | 89 | 92 | 93 | 96 | 97 | 100 |
101 | 102 |
103 |
104 |
105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 |
115 | 116 | 117 |

118 | {tr:main_title_expand} 119 |

120 | 121 | 122 |

123 | {tr:home_disclaimer} 124 |

125 | 126 |
    127 |
  • {tr:home_browser}
  • 128 |
  • {tr:home_devices}
  • 129 |
  • {tr:home_camera}
  • 130 |
  • {tr:home_micro}
  • 131 |
  • {tr:home_network}
  • 132 |
  • {tr:home_room}
  • 133 |
134 | 135 | 136 |
137 | 138 | {tr:run_all_tests} 139 | 140 | 141 |
142 | 143 | 144 | 145 |
146 |
147 |
148 | 149 | 150 | 151 |
152 |

153 | {tr:browser_title} 154 |
155 | 156 | 157 | OK 158 | KO 159 | 160 |
161 |

162 | 163 | 164 |
165 | {tr:run_alone_test} 166 | 167 | 168 | {tr:run} 169 | 170 |
171 |
172 |
{tr:results_shown_there}
173 |
174 |
{tr:browser_test_success}
175 |
176 |
{tr:browser_test_fail}
177 |
178 |
179 | 180 | 181 |
182 |

183 | {tr:devices_title} 184 |
185 | 186 | 187 | OK 188 | KO 189 | 190 |
191 |

192 | 193 |
194 | {tr:run_alone_test} 195 | 196 | 197 | {tr:run} 198 | 199 |
200 |
201 |
{tr:results_shown_there}
202 |
203 | {tr:devices_test_success} 204 |
205 |
206 |
{tr:devices_audio_input_label}
207 |
208 |
209 |
210 |
{tr:devices_audio_output_label}
211 |
212 |
213 |
214 |
{tr:devices_video_label}
215 |
216 |
217 |
218 |
219 |
{tr:devices_test_fail}
220 |
221 |
222 | 223 | 224 |
225 |

226 | {tr:camera_title} 227 |
228 | 229 | 230 | OK 231 | KO 232 | 233 |
234 |

235 | 236 |
237 | {tr:run_alone_test} 238 | 239 | 240 | {tr:run} 241 | 242 |
243 |
244 |
{tr:results_shown_there}
245 |
246 |
{tr:camera_test_success}
247 |
{tr:camera_test_success_default}
248 |
249 | 250 |
251 |
252 |
{tr:camera_test_fail}
253 |
254 |
255 | 256 | 257 |
258 |

259 | {tr:micro_title} 260 |
261 | 262 | 263 | OK 264 | KO 265 | 266 |
267 |

268 | 269 |
270 | {tr:run_alone_test} 271 | 272 | 273 | {tr:run} 274 | 275 |
276 |
277 |
{tr:results_shown_there}
278 |
279 |
{tr:micro_test_success}
280 |
{tr:micro_test_success_default}
281 |
282 | 283 |
284 |
285 |
{tr:camera_test_fail}
286 |
287 |
288 | 289 | 290 |
291 | 292 |
293 |

294 | {tr:network_title} 295 |
296 | 297 | 298 | OK 299 | KO 300 | 301 |
302 |

303 | 304 |
305 | {tr:run_alone_test} 306 | 307 | 308 | {tr:run} 309 | 310 |
311 | 312 |
313 |
314 |
Local video
315 | 316 |
317 | Video dimensions: 318 | N/C 319 |
320 |
321 |
322 |
Remote video
323 | 324 |
325 | Video dimensions: 326 | N/C 327 |
328 |
329 | Connected to: 330 | N/C 331 |
332 |
333 |
334 |
335 | 336 |
337 |
338 | 339 | {tr:websocket} 340 | 341 | 342 | 343 | 344 | 345 |
346 |
347 | 348 | {tr:udp} 349 | 350 | 351 | 352 | 353 | 354 |
355 | 356 |
357 | 358 | {tr:tcp} 359 | 360 | 361 | 362 | 363 | 364 |
365 | 366 |
367 | 368 | {tr:bitrate} 369 | 0 kbit/s 370 | 371 | 372 | {tr:average_bitrate} 373 | 0 kbit/s 374 | 375 | 376 | 377 | 378 |
379 |
380 | 381 | {tr:packetlost} 382 | 0 383 | 384 | 385 | 386 | 387 |
388 |
389 | 390 | {tr:framerate} 391 | 0 392 | 393 | 394 | 395 | 396 |
397 |
398 | 399 | {tr:droppedframes} 400 | 0 401 | 402 | 403 | 404 | 405 |
406 |
407 | 408 | {tr:jitter} 409 | 410 | 411 | 412 | 0 413 | 414 |
415 |
416 | 417 |
418 |
{tr:results_shown_there}
419 |
420 |
{tr:network_test_success}
421 |
422 |
423 | {tr:error} 424 | {tr:network_test_fail} 425 |
426 | 427 |
428 |
429 |
430 | 431 |
432 | 433 | 434 |
435 |

436 | {tr:room_title} 437 |
438 | 439 | 440 | OK 441 | KO 442 | 443 |
444 |

445 | 446 |
447 | {tr:run_alone_test} 448 | 449 | 450 | {tr:run} 451 | 452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 | 464 |
465 |
{tr:results_shown_there}
466 |
467 |
{tr:room_test_success}
468 |
469 |
470 | {tr:error} 471 | {tr:room_test_fail} 472 |
473 | 474 |
475 |
476 |
477 |
478 | 479 | 480 |
481 |

{tr:results}

482 | 483 | 484 |
485 |
{tr:all_results_shown_there}
486 |
487 |

{tr:global_test_success}

488 |

{tr:global_test_message}

489 |
490 |
491 |

{tr:global_test_fail}

492 | 493 |

{tr:following_test_failed}

494 |
    495 |
  • {tr:home_devices}
  • 496 |
  • {tr:home_camera}
  • 497 |
  • {tr:home_micro}
  • 498 |
  • {tr:home_network}
  • 499 |
  • {tr:home_room}
  • 500 |
501 |

{tr:global_more_information}

502 |
503 | 504 | 512 |
513 |
514 |
515 | 516 | 517 |
518 | 519 | -------------------------------------------------------------------------------- /src/assets/js/test_browser/test_cases/test_network.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TestCase: test_network 3 | */ 4 | 5 | if (!window.hasOwnProperty('JitsiTestBrowser')) 6 | window.JitsiTestBrowser = {}; 7 | 8 | if (!window.JitsiTestBrowser.hasOwnProperty('test_network')) 9 | window.JitsiTestBrowser.test_network = {}; 10 | 11 | 12 | /** 13 | * Test network case 14 | */ 15 | window.JitsiTestBrowser.test_network = { 16 | 17 | /** 18 | * TURN credentials 19 | */ 20 | turn_credentials: [], 21 | 22 | /** 23 | * TURN servers 24 | */ 25 | turn_servers: [], 26 | 27 | 28 | /** 29 | * Global local media stream object 30 | */ 31 | localStream: undefined, 32 | 33 | /** 34 | * Local peer connection 35 | */ 36 | localPeerConnection: undefined, 37 | 38 | /** 39 | * Remote peer connection 40 | */ 41 | remotePeerConnection: undefined, 42 | 43 | 44 | /** 45 | * Local video 46 | */ 47 | localVideo: undefined, 48 | 49 | /** 50 | * Remote video 51 | */ 52 | remoteVideo: undefined, 53 | 54 | /** 55 | * Tests statuses 56 | */ 57 | statuses: {}, 58 | 59 | /** 60 | * Interval ID to stop get stats interval 61 | */ 62 | intervalID: undefined, 63 | 64 | exceptions: null, 65 | 66 | 67 | /** 68 | * Variables used to get RTC stats 69 | */ 70 | testing_protocol: undefined, 71 | bytesPrev: undefined, 72 | timestampPrev: undefined, 73 | stats: { 74 | tcp:{ 75 | bitrate: [], 76 | packetsLost: 0, 77 | framesPerSecond: [], 78 | droppedFrames: 0, 79 | jitter: [] 80 | }, 81 | udp:{ 82 | bitrate: [], 83 | packetsLost: 0, 84 | framesPerSecond: [], 85 | droppedFrames: 0, 86 | jitter: [] 87 | }, 88 | video:{ 89 | local: [], 90 | remote: [] 91 | } 92 | }, 93 | 94 | networkEvent: new Event('network_stat'), 95 | 96 | /** 97 | * Run test 98 | * 99 | * @return {Promise} 100 | */ 101 | run: function () { 102 | return new Promise(res => { 103 | console.log("> Running test_network"); 104 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.PROCESSING, "context": "test_network"}); 105 | 106 | let context = window.JitsiTestBrowser.test_network; 107 | 108 | context.localVideo = document.querySelector('video#local_video'); 109 | context.remoteVideo = document.querySelector('video#remote_video'); 110 | context.exceptions = []; 111 | 112 | // Init TURN credentials 113 | context.initTURNCredentials(function(result){ 114 | if (result.status === "fail"){ 115 | window.JitsiTestEvents.dispatch('network_stat', {"context": "wss", "data": result}); 116 | } 117 | 118 | // Run websocket test 119 | context.testWebSocket().then(function (result) { 120 | 121 | // Run UDP test 122 | context.testUDP().then(function () { 123 | // Stop getting statistics 124 | if (context.intervalID) clearInterval(context.intervalID) 125 | 126 | // Reset stats 127 | context.bytesPrev = 0; 128 | context.timestampPrev = 0; 129 | 130 | // Close connection 131 | context.hangup(); 132 | 133 | // Next: test TCP protocol 134 | context.testTCP().then(function () { 135 | // Stop getting statistics 136 | if (context.intervalID) clearInterval(context.intervalID) 137 | 138 | // Reset stats 139 | context.bytesPrev = 0; 140 | context.timestampPrev = 0; 141 | 142 | // Close connection 143 | context.hangup(); 144 | 145 | // Show final test results 146 | let allOK = Object.keys(context.statuses).length > 0; 147 | 148 | Object.keys(context.statuses).forEach(status => { 149 | if (context.statuses[status] === false){ 150 | allOK = false; 151 | context.statuses[status] = "fail" 152 | }else{ 153 | context.statuses[status] = "success"; 154 | } 155 | }); 156 | 157 | // Push statistics 158 | context.pushStatistics(); 159 | 160 | window.JitsiTestBrowser.runner.resolve(res, {"result": allOK ? "success" : "fail"}, "test_network"); 161 | }) 162 | }); 163 | }); 164 | }, function(reason){ 165 | window.JitsiTestBrowser.runner.resolve(res, {"result": "fail", "details": reason}, "test_network"); 166 | }); 167 | 168 | }); 169 | }, 170 | 171 | /** 172 | * Get network statistics 173 | */ 174 | getNetworkStatistics: function () { 175 | let context = window.JitsiTestBrowser.test_network; 176 | 177 | context.intervalID = setInterval(() => { 178 | if (context.localPeerConnection && context.remotePeerConnection) { 179 | context.remotePeerConnection 180 | .getStats(null) 181 | .then(context.showRemoteStats, err => { 182 | console.log(err); 183 | throw err; 184 | }); 185 | context.localPeerConnection 186 | .getStats(null) 187 | .then(context.showLocalStats, err => { 188 | console.log(err); 189 | throw err; 190 | }); 191 | } else { 192 | console.log('Not connected yet'); 193 | } 194 | // Collect some stats from the video tags. 195 | if (context.localVideo.videoWidth) { 196 | const width = context.localVideo.videoWidth; 197 | const height = context.localVideo.videoHeight; 198 | 199 | context.stats['video']['local'] = {video_dimension: {"width": width, "height": height}}; 200 | 201 | window.JitsiTestEvents.dispatch('network_stat', {"context":"video_player", "data": {"local": {"video_dimension": {"width": width, "height": height}}}}); 202 | } 203 | if (context.remoteVideo.videoWidth) { 204 | const rHeight = context.remoteVideo.videoHeight; 205 | const rWidth = context.remoteVideo.videoWidth; 206 | 207 | context.stats['video']['remote'] = {video_dimension: {"width": rWidth, "height": rHeight}}; 208 | 209 | window.JitsiTestEvents.dispatch('network_stat', {"context":"video_player", "data": {"remote": {"video_dimension": {"width": rWidth, "height": rHeight}}}}); 210 | } 211 | }, 1000); 212 | }, 213 | 214 | /** 215 | * Init TURN credentials 216 | */ 217 | initTURNCredentials: function (resolve, reject) { 218 | let context = window.JitsiTestBrowser.test_network; 219 | 220 | let turnServer = document.getElementById('main').getAttribute('data-turn-endpoint'); 221 | 222 | let settings = { 223 | method: 'get', 224 | }; 225 | 226 | fetch(turnServer, settings) 227 | .then(response => { 228 | response.json() 229 | .then(function (data) { 230 | context.turn_servers = { 231 | "username": data.username, 232 | "credential": data.credential, 233 | "tcp_urls": data.tcpTestUrl, 234 | "udp_urls": data.udpTestUrl, 235 | }; 236 | resolve({"status": "success"}); 237 | }) 238 | } 239 | ) 240 | .catch(reason => { 241 | resolve({"status": "fail", "details": reason.toString()}); 242 | }); 243 | }, 244 | 245 | 246 | // Test functions 247 | 248 | /** 249 | * Test web socket 250 | * 251 | * @return {Promise} 252 | */ 253 | testWebSocket: function () { 254 | return new Promise(resolve => { 255 | console.log(" >>> Test WebSocket connection"); 256 | 257 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.PROCESSING, "component": "wss"}); 258 | 259 | let context = window.JitsiTestBrowser.test_network; 260 | context.testing_protocol = 'wss'; 261 | 262 | let wssUrl = document.getElementById('main').getAttribute('data-websocket-url'); 263 | 264 | let wbs = new WebSocket(wssUrl); 265 | let expected = 'this is a connection test'; 266 | 267 | wbs.onopen = function () { 268 | console.log('WSS connection established'); 269 | wbs.send(expected); 270 | }; 271 | 272 | wbs.onmessage = function (messageEvent) { 273 | console.log(`Got data from WSS: ${messageEvent.data}`); 274 | let result; 275 | 276 | if (messageEvent.data === `echo ${expected}`) { 277 | wbs.close(); 278 | context.statuses['wss'] = true; 279 | result = {"status": "success"}; 280 | 281 | } else { 282 | context.statuses['wss'] = false; 283 | wbs.close(); 284 | result = {"status": "fail", "details": {"protocol": "wss", "message": messageEvent.data}}; 285 | context.testFail(err); 286 | resolve(err); 287 | } 288 | 289 | window.JitsiTestEvents.dispatch('network_stat', {"context":"wss", "data": result}); 290 | 291 | resolve(result); 292 | }; 293 | 294 | wbs.onerror = function () { 295 | context.statuses['wss'] = false; 296 | wbs.close(); 297 | let err = {"status": "fail", "details": {"protocol": "wss", "details": 'cannot_connect_wss'}}; 298 | context.testFail('wss', err); 299 | resolve(err); 300 | }; 301 | }); 302 | }, 303 | 304 | /** 305 | * Test TCP 306 | * 307 | * @return {Promise} 308 | */ 309 | testTCP: function () { 310 | return new Promise(resolve => { 311 | console.log(" >>> Test TCP media network"); 312 | 313 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.PROCESSING, "component":"tcp"}); 314 | 315 | let context = window.JitsiTestBrowser.test_network; 316 | context.testing_protocol = 'tcp'; 317 | 318 | // Start getting network statistics 319 | context.getNetworkStatistics(); 320 | 321 | context.initiateMediaConnexion("tcp").then(function (result) { 322 | if (result instanceof Object && result.status === 'success') { 323 | let utils = new WebRTCUtils(); 324 | utils.wait(5000).then(function () { 325 | 326 | window.JitsiTestEvents.dispatch('network_stat', {"context": "tcp", "data": result}); 327 | 328 | resolve(); 329 | }); 330 | } else { 331 | context.testFail('tcp', result); 332 | 333 | window.JitsiTestEvents.dispatch('network_stat', {"context": "tcp", "data": result}); 334 | 335 | resolve(); 336 | } 337 | }); 338 | }); 339 | }, 340 | 341 | 342 | 343 | /** 344 | * Test UDP 345 | * 346 | * @return {Promise} 347 | */ 348 | testUDP: function () { 349 | return new Promise(resolve => { 350 | console.log(" >>> Test UDP media network"); 351 | 352 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.PROCESSING, "component": "udp"}); 353 | 354 | let context = window.JitsiTestBrowser.test_network; 355 | context.testing_protocol = 'udp'; 356 | 357 | // Start getting network statistics 358 | context.getNetworkStatistics(); 359 | 360 | context.initiateMediaConnexion("udp") 361 | .then(function (result) { 362 | if (result instanceof Object && result.status === 'success') { 363 | let utils = new WebRTCUtils(); 364 | utils.wait(5000).then(function () { 365 | 366 | window.JitsiTestEvents.dispatch('network_stat', {"context":"udp", "data": result}); 367 | 368 | resolve({"status": "success", "details": {"protocol": "udp"}}); 369 | }); 370 | } else { 371 | context.testFail('udp', result); 372 | 373 | window.JitsiTestEvents.dispatch('network_stat', {"context":"udp", "data": result}); 374 | 375 | resolve(); 376 | } 377 | } 378 | ); 379 | }); 380 | }, 381 | 382 | 383 | // WebRTC functions 384 | 385 | /** 386 | * Initiate a media connexion for protocol 387 | * 388 | * @param protocol 389 | */ 390 | initiateMediaConnexion: function (protocol) { 391 | return new Promise(resolve => { 392 | if (protocol !== 'tcp' && protocol !== 'udp') { 393 | resolve({"status": "fail", "details": {"message": "unknown_protocol"}}) 394 | 395 | } else { 396 | let context = window.JitsiTestBrowser.test_network; 397 | 398 | if (context.localStream) { 399 | context.localStream.getTracks().forEach(track => track.stop()); 400 | const videoTracks = context.localStream.getVideoTracks(); 401 | for (let i = 0; i !== videoTracks.length; ++i) { 402 | videoTracks[i].stop(); 403 | } 404 | } 405 | 406 | // Init media constraints 407 | // Should be in config? 408 | let mediaStreamConstraints = { 409 | video: { 410 | width: {min: 300, max: 400, ideal: 320}, 411 | height: {min: 150, max: 300, ideal: 240}, 412 | frameRate: {min: 10, max: 60,} 413 | }, 414 | audio: true 415 | }; 416 | let rtcConfig = { 417 | iceTransportPolicy: 'relay' 418 | }; 419 | let servers = { 420 | "username": context.turn_servers.username, 421 | "credential": context.turn_servers.credential, 422 | "urls": protocol === 'tcp' ? context.turn_servers.tcp_urls : context.turn_servers.udp_urls 423 | }; 424 | rtcConfig.iceServers = [servers]; 425 | 426 | // Get user media 427 | navigator.mediaDevices.getUserMedia(mediaStreamConstraints) 428 | .then(function (mediaStream) { 429 | /* use the stream */ 430 | console.log('GetUserMedia succeeded'); 431 | context.localStream = mediaStream; 432 | context.localVideo.srcObject = mediaStream; 433 | context.createPeerConnection(rtcConfig) 434 | context.statuses[protocol] = true; 435 | resolve({"status": "success", "details": {"message": "GetUserMedia succeeded"}}) 436 | }) 437 | .catch(function (err) { 438 | resolve({"status": "fail", "details": err.toString()}); 439 | }) 440 | } 441 | }); 442 | }, 443 | 444 | 445 | /** 446 | * Create peer connection 447 | * 448 | * @param rtcConfig 449 | */ 450 | createPeerConnection: function (rtcConfig = null) { 451 | console.log('Call Start'); 452 | let context = window.JitsiTestBrowser.test_network; 453 | try { 454 | context.localPeerConnection = new RTCPeerConnection(rtcConfig); 455 | context.remotePeerConnection = new RTCPeerConnection(rtcConfig); 456 | context.localStream.getTracks().forEach(track => context.localPeerConnection.addTrack(track, context.localStream)); 457 | console.log('localPeerConnection creating offer'); 458 | context.localPeerConnection.onnegotiationneeded = () => console.log('Negotiation needed - localPeerConnection'); 459 | context.remotePeerConnection.onnegotiationneeded = () => console.log('Negotiation needed - remotePeerConnection'); 460 | 461 | context.localPeerConnection.onicecandidate = e => { 462 | console.log('Candidate localPeerConnection'); 463 | if (e.candidate && e.candidate.candidate){ 464 | let clientIP = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/.exec(e.candidate.candidate)[1]; 465 | console.log('Client IP: ', clientIP); 466 | context.stats[context.testing_protocol]['client_ip'] = clientIP; 467 | } 468 | 469 | context.remotePeerConnection 470 | .addIceCandidate(e.candidate) 471 | .then(context.onAddIceCandidateSuccess, context.onAddIceCandidateError); 472 | }; 473 | context.localPeerConnection.oniceconnectionstatechange = function(){ 474 | context.oniceconnectionstatechange('localPeerConnection', context.localPeerConnection) 475 | }; 476 | context.remotePeerConnection.oniceconnectionstatechange = function(){ 477 | context.oniceconnectionstatechange('remotePeerConnection', context.remotePeerConnection) 478 | }; 479 | context.localPeerConnection.onsignalingstatechange = function(){ 480 | context.onsignalingstatechange('localPeerConnection', context.localPeerConnection) 481 | }; 482 | context.remotePeerConnection.onsignalingstatechange = function(){ 483 | context.onsignalingstatechange('remotePeerConnection', context.remotePeerConnection) 484 | }; 485 | context.remotePeerConnection.onicecandidate = e => { 486 | console.log('Candidate remotePeerConnection'); 487 | context.localPeerConnection 488 | .addIceCandidate(e.candidate) 489 | .then(context.onAddIceCandidateSuccess, context.onAddIceCandidateError); 490 | }; 491 | context.remotePeerConnection.ontrack = e => { 492 | if (context.remoteVideo.srcObject !== e.streams[0]) { 493 | console.log('remotePeerConnection got stream'); 494 | context.remoteVideo.srcObject = e.streams[0]; 495 | } 496 | }; 497 | context.localPeerConnection.createOffer().then( 498 | offer => { 499 | console.log('localPeerConnection offering'); 500 | console.log(`localPeerConnection offer: ${offer.sdp}`); 501 | context.localPeerConnection.setLocalDescription(offer); 502 | context.remotePeerConnection.setRemoteDescription(offer); 503 | context.remotePeerConnection.createAnswer().then( 504 | answer => { 505 | console.log('remotePeerConnection answering'); 506 | console.log(`remotePeerConnection answer: ${answer.sdp}`); 507 | context.remotePeerConnection.setLocalDescription(answer); 508 | context.localPeerConnection.setRemoteDescription(answer); 509 | }, 510 | err => function () { 511 | console.log(err) 512 | throw err; 513 | } 514 | ); 515 | }, 516 | err => function () { 517 | console.log(err) 518 | throw err; 519 | } 520 | ); 521 | } catch (err) { 522 | console.log(err) 523 | throw err; 524 | } 525 | }, 526 | 527 | onAddIceCandidateSuccess: function () { 528 | // TODO: show on UI? 529 | console.log('AddIceCandidate success.'); 530 | }, 531 | 532 | onAddIceCandidateError: function (error) { 533 | // TODO: show on UI? 534 | console.error(`Failed to add Ice Candidate: ${error.toString()}`); 535 | }, 536 | 537 | 538 | showLocalStats: function (results) { 539 | // Nothing useful to show for now 540 | // Keep this function just in case 541 | }, 542 | 543 | /** 544 | * Handle network stats got from remove video 545 | * 546 | * @param results 547 | */ 548 | showRemoteStats: function (results) { 549 | console.log(results); 550 | let context = window.JitsiTestBrowser.test_network; 551 | 552 | // calculate video bitrate 553 | results.forEach(report => { 554 | const now = report.timestamp; 555 | window.JitsiTestEvents.networkStat.context = context.testing_protocol; 556 | 557 | let bitrate; 558 | if (report.type === 'inbound-rtp' && report.mediaType === 'video') { 559 | const bytes = report.bytesReceived; 560 | if (context.timestampPrev) { 561 | bitrate = 8 * (bytes - context.bytesPrev) / (now - context.timestampPrev); 562 | bitrate = Math.floor(bitrate); 563 | } 564 | context.bytesPrev = bytes; 565 | context.timestampPrev = now; 566 | } 567 | if (bitrate) { 568 | context.stats[context.testing_protocol].bitrate.push(bitrate); 569 | 570 | window.JitsiTestEvents.dispatch('network_stat', {"data": {"bitrate": bitrate}}); 571 | 572 | if (context.stats[context.testing_protocol].bitrate.length) { 573 | let average = 0; 574 | context.stats[context.testing_protocol].bitrate.forEach(bt => { 575 | average += bt; 576 | }); 577 | average = (average / context.stats[context.testing_protocol].bitrate.length).toFixed(2); 578 | 579 | window.JitsiTestEvents.dispatch('network_stat', {"data": {"average_bitrate": average}}); 580 | } 581 | } 582 | 583 | // Dispatch statistics 584 | ['framesPerSecond', 'framesDropped', 'packetsLost', 'jitter'].forEach(item => { 585 | if (report[item] !== undefined) { 586 | let data = {}; 587 | data[item] = report[item]; 588 | 589 | context.stats[context.testing_protocol][item] = report[item]; 590 | 591 | window.JitsiTestEvents.dispatch('network_stat', {data}); 592 | } 593 | }); 594 | }); 595 | 596 | // figure out the peer's ip 597 | let activeCandidatePair = null; 598 | let remoteCandidate = null; 599 | 600 | // Search for the candidate pair, spec-way first. 601 | results.forEach(report => { 602 | if (report.type === 'transport') { 603 | activeCandidatePair = results.get(report.selectedCandidatePairId); 604 | } 605 | }); 606 | // Fallback for Firefox. 607 | if (!activeCandidatePair) { 608 | results.forEach(report => { 609 | if (report.type === 'candidate-pair' && report.selected) { 610 | activeCandidatePair = report; 611 | } 612 | }); 613 | } 614 | if (activeCandidatePair && activeCandidatePair.remoteCandidateId) { 615 | remoteCandidate = results.get(activeCandidatePair.remoteCandidateId); 616 | } 617 | if (remoteCandidate) { 618 | if (remoteCandidate.address && remoteCandidate.port) { 619 | context.stats[context.testing_protocol]['ip_connected_to'] = `${remoteCandidate.address}:${remoteCandidate.port}`; 620 | window.JitsiTestEvents.dispatch('network_stat', {"data": {"ip_connected_to": `${remoteCandidate.address}:${remoteCandidate.port}`}}); 621 | 622 | } else if (remoteCandidate.ip && remoteCandidate.port) { 623 | context.stats[context.testing_protocol]['ip_connected_to'] = `${remoteCandidate.address}:${remoteCandidate.port}`; 624 | window.JitsiTestEvents.dispatch('network_stat', {"data": {"ip_connected_to": `${remoteCandidate.ip}:${remoteCandidate.port}`}}); 625 | 626 | } else if (remoteCandidate.ipAddress && remoteCandidate.portNumber) { 627 | context.stats[context.testing_protocol]['ip_connected_to'] = `${remoteCandidate.address}:${remoteCandidate.port}`; 628 | window.JitsiTestEvents.dispatch('network_stat', {"data": {"ip_connected_to": `${remoteCandidate.ipAddress}:${remoteCandidate.portNumber}`}}); 629 | } 630 | } 631 | }, 632 | 633 | /** 634 | * Close the 2 active peerConnection localPeerConnection and remotePeerConnection and release media capture 635 | */ 636 | hangup: function () { 637 | let context = window.JitsiTestBrowser.test_network; 638 | if (context.localPeerConnection) { 639 | console.log("localPeerConnection iceConnectionState :" + context.localPeerConnection.iceConnectionState); 640 | console.log("remotePeerConnection iceConnectionState :" + context.remotePeerConnection.iceConnectionState); 641 | 642 | context.stateEnabled = false; 643 | } 644 | if (context.localPeerConnection) { 645 | context.localPeerConnection.close(); 646 | } 647 | if (context.remotePeerConnection) { 648 | context.remotePeerConnection.close(); 649 | } 650 | if (context.localStream) { 651 | context.localStream.getTracks().forEach(function (track) { 652 | track.stop(); 653 | }); 654 | } 655 | 656 | context.localVideo.srcObject = null; 657 | context.remoteVideo.srcObject = null; 658 | 659 | context.localPeerConnection = undefined; 660 | context.remotePeerConnection = undefined; 661 | }, 662 | 663 | onsignalingstatechange: function (peerConnectionName, peerConnection) { 664 | console.log(peerConnectionName + ' ICE: ' + peerConnection.iceConnectionState); 665 | }, 666 | 667 | oniceconnectionstatechange: function (peerConnectionName, peerConnection) { 668 | console.log(peerConnectionName + ' ICE: ' + peerConnection.iceConnectionState); 669 | }, 670 | 671 | onicecandidate: function (peerConnectionName, peerConnection, event) { 672 | if (event.candidate) { 673 | peerConnection.addIceCandidate(event.candidate); 674 | console.log(peerConnectionName + ' ICE Candidate: ' + event.candidate.candidate); 675 | } 676 | }, 677 | 678 | 679 | /** 680 | * Push final statistics 681 | */ 682 | pushStatistics: function(){ 683 | let context = window.JitsiTestBrowser.test_network; 684 | 685 | let stats = { 686 | "wss": { 687 | "status" : context.statuses['wss'], 688 | }, 689 | "tcp": { 690 | "status" : context.statuses['tcp'], 691 | "data": context.stats['tcp'] 692 | 693 | }, 694 | "udp": { 695 | "status" : context.statuses['udp'], 696 | "data": context.stats['udp'] 697 | }, 698 | "video":{ 699 | "local": context.stats['video']['local'], 700 | "remote": context.stats['video']['local'] 701 | } 702 | }; 703 | 704 | let protocols = ['wss', 'tcp', 'udp'] 705 | protocols.forEach(protocol => { 706 | if (context.exceptions.hasOwnProperty(protocol)){ 707 | stats[protocol].exception = context.exceptions[protocol]; 708 | } 709 | }) 710 | 711 | 712 | window.JitsiTestBrowser.Statistics.addStat('test_network', stats ); 713 | }, 714 | 715 | /** 716 | * Default test fail handler 717 | * 718 | * @param protocol 719 | * @param result 720 | */ 721 | testFail: function (protocol, result) { 722 | let context = window.JitsiTestBrowser.test_network; 723 | 724 | context.statuses[protocol] = false; 725 | 726 | let details = result; 727 | if (result instanceof Error) { 728 | details = result.toString(); 729 | } else if (result instanceof Object) { 730 | details = JSON.stringify(result); 731 | } 732 | 733 | context.exceptions[protocol] = result; 734 | 735 | console.error(details); 736 | 737 | window.JitsiTestEvents.dispatch('network_stat', {"context": protocol, "data": result}); 738 | }, 739 | 740 | 741 | /** 742 | * Reset UI elements 743 | */ 744 | reset: function (){ 745 | let container = document.getElementById('network_pane'); 746 | 747 | // Reset stat right items 748 | container.querySelectorAll('div.stat-right-item').forEach(function(element){ 749 | element.classList.remove('test-fail', 'test-success'); 750 | }); 751 | 752 | // Hide status icon 753 | container.querySelectorAll('[data-content="status_icon"]').forEach(function(element){ 754 | element.classList.add('hide'); 755 | }); 756 | 757 | // Reset stat values 758 | ['media_connectivity_packetlost', 'media_connectivity_framerate', 'media_connectivity_droppedframes', 'media_connectivity_jitter'] 759 | .forEach(function (element){ 760 | document.getElementById(element).querySelector(`span[data-content="value"]`).textContent = '0'; 761 | }); 762 | document.querySelectorAll('[data-content="video_dimensions"] span[data-content="value"] ,div[data-content="ip_connected_to"] span[data-content="value"]') 763 | .forEach(function (element){ 764 | element.textContent = 'N/C' 765 | }); 766 | }, 767 | 768 | 769 | /** 770 | * Final resolve function 771 | * 772 | * @param res 773 | * @param data 774 | */ 775 | resolve: function(res, data){ 776 | window.JitsiTestEvents.dispatch('run', {"status": window.TestStatuses.ENDED, "context": "test_network"}); 777 | res(data) 778 | } 779 | } --------------------------------------------------------------------------------