This is an automated email from the ASF dual-hosted git repository. solomax pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/openmeetings.git
The following commit(s) were added to refs/heads/master by this push: new 2d3e407 [OPENMEETINGS-2000] moving JS code to npm 2d3e407 is described below commit 2d3e4079795a736893fe1a06fdecc73a0eacc74c Author: Maxim Solodovnik <solomax...@gmail.com> AuthorDate: Thu Dec 24 13:41:16 2020 +0700 [OPENMEETINGS-2000] moving JS code to npm --- .../src/main/assembly/components/templates.xml | 2 - openmeetings-web/pom.xml | 47 +- openmeetings-web/src/main/front/room/src/volume.js | 121 +++++ .../src/main/front/settings/package.json | 22 + .../src/main/front/settings/src/index.js | 15 + .../src/main/front/settings/src/mic-level.js | 91 ++++ .../src/main/front/settings/src/ring-buffer.js | 18 + .../src/main/front/settings/src/settings.js | 497 +++++++++++++++++ .../src/main/front/settings/src/video-util.js | 333 ++++++++++++ .../apache/openmeetings/web/room/raw-settings.js | 605 --------------------- .../apache/openmeetings/web/room/raw-video-util.js | 445 --------------- .../org/apache/openmeetings/web/room/raw-video.js | 2 +- pom.xml | 2 +- 13 files changed, 1117 insertions(+), 1083 deletions(-) diff --git a/openmeetings-server/src/main/assembly/components/templates.xml b/openmeetings-server/src/main/assembly/components/templates.xml index 7e7ba2d..43f059c 100644 --- a/openmeetings-server/src/main/assembly/components/templates.xml +++ b/openmeetings-server/src/main/assembly/components/templates.xml @@ -35,8 +35,6 @@ <exclude>**/raw-*.js</exclude> <exclude>**/fabric.js</exclude> <exclude>**/MathJax*.js</exclude> - <exclude>**/adapter-latest.js</exclude> - <exclude>**/kurento-utils.js</exclude> <exclude>**/NoSleep.js</exclude> </excludes> </fileSet> diff --git a/openmeetings-web/pom.xml b/openmeetings-web/pom.xml index c128ca1..50b654b 100644 --- a/openmeetings-web/pom.xml +++ b/openmeetings-web/pom.xml @@ -102,6 +102,24 @@ </environmentVariables> </configuration> </execution> + <execution> + <id>settings-install</id> + <goals><goal>npm</goal></goals> + <configuration> + <workingDirectory>src/main/front/main</workingDirectory> + </configuration> + </execution> + <execution> + <id>settings</id> + <goals><goal>npm</goal></goals> + <configuration> + <arguments>run build</arguments> + <workingDirectory>src/main/front/main</workingDirectory> + <environmentVariables> + <outDir>${project.build.directory}/generated-sources/js/org/apache/openmeetings/web/room/</outDir> + </environmentVariables> + </configuration> + </execution> </executions> </plugin> <plugin> @@ -221,25 +239,6 @@ </configuration> </execution> <execution> - <id>settings-js</id> - <goals> - <goal>minify</goal> - </goals> - <configuration> - <charset>UTF-8</charset> - <jsSourceDir>../java/org/apache/openmeetings/web/room</jsSourceDir> - <jsSourceFiles> - <jsSourceFile>raw-video-util.js</jsSourceFile> - <jsSourceFile>raw-settings.js</jsSourceFile> - <jsSourceFile>adapter-latest.js</jsSourceFile> - <jsSourceFile>kurento-utils.js</jsSourceFile> - </jsSourceFiles> - <jsFinalFile>settings.js</jsFinalFile> - <jsEngine>CLOSURE</jsEngine> - <jsTargetDir>../generated-sources/js/org/apache/openmeetings/web/room</jsTargetDir> - </configuration> - </execution> - <execution> <id>nettest-js</id> <goals> <goal>minify</goal> @@ -285,8 +284,6 @@ **/raw-*.js, **/fabric.js, **/MathJax*.js, - **/adapter-latest.js, - **/kurento-utils.js, **/NoSleep.js </packagingExcludes> <warSourceExcludes> @@ -294,8 +291,6 @@ **/raw-*.js, **/fabric.js, **/MathJax*.js, - **/adapter-latest.js, - **/kurento-utils.js, **/NoSleep.js </warSourceExcludes> <filteringDeploymentDescriptors>true</filteringDeploymentDescriptors> @@ -314,8 +309,6 @@ <exclude>**/raw-*.js</exclude> <exclude>**/fabric.js</exclude> <exclude>**/MathJax*.js</exclude> - <exclude>**/adapter-latest.js</exclude> - <exclude>**/kurento-utils.js</exclude> <exclude>**/NoSleep.js</exclude> </excludes> </webResource> @@ -430,8 +423,6 @@ <exclude>**/raw-*.js</exclude> <exclude>**/fabric.js</exclude> <exclude>**/MathJax*.js</exclude> - <exclude>**/adapter-latest.js</exclude> - <exclude>**/kurento-utils.js</exclude> <exclude>**/network.js</exclude> </excludes> <filtering>true</filtering> @@ -463,8 +454,6 @@ <exclude>**/raw-*.js</exclude> <exclude>**/fabric.js</exclude> <exclude>**/MathJax*.js</exclude> - <exclude>**/adapter-latest.js</exclude> - <exclude>**/kurento-utils.js</exclude> <exclude>**/network.js</exclude> </excludes> </resource> diff --git a/openmeetings-web/src/main/front/room/src/volume.js b/openmeetings-web/src/main/front/room/src/volume.js new file mode 100644 index 0000000..65fa822 --- /dev/null +++ b/openmeetings-web/src/main/front/room/src/volume.js @@ -0,0 +1,121 @@ +/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */ +var Volume = (function() { + let video, vol, drop, slider, handleEl, hideTimer = null + , lastVolume = 50, muted = false; + + function __cancelHide() { + if (hideTimer) { + clearTimeout(hideTimer); + hideTimer = null; + } + } + function __hideDrop() { + __cancelHide(); + hideTimer = setTimeout(() => { + drop.hide(); + hideTimer = null; + }, 3000); + } + + function _create(_video) { + video = _video; + _destroy(); + const uid = video.stream().uid + , cuid = video.stream().cuid + , volId = 'volume-' + uid; + vol = OmUtil.tmpl('#volume-control-stub', volId) + slider = vol.find('.slider'); + drop = vol.find('.dropdown-menu'); + vol.on('mouseenter', function(e) { + e.stopImmediatePropagation(); + drop.show(); + __hideDrop() + }) + .click(function(e) { + e.stopImmediatePropagation(); + OmUtil.roomAction({action: 'mute', uid: cuid, mute: !muted}); + _mute(!muted); + drop.hide(); + return false; + }).dblclick(function(e) { + e.stopImmediatePropagation(); + return false; + }); + drop.on('mouseenter', function() { + __cancelHide(); + }); + drop.on('mouseleave', function() { + __hideDrop(); + }); + handleEl = vol.find('.handle'); + slider.slider({ + orientation: 'vertical' + , range: 'min' + , min: 0 + , max: 100 + , value: lastVolume + , create: function() { + handleEl.text($(this).slider('value')); + } + , slide: function(event, ui) { + _handle(ui.value); + } + }); + _handle(lastVolume); + _mute(muted); + return vol; + } + function _handle(val) { + handleEl.text(val); + const vidEl = video.video() + , data = vidEl.data(); + if (video.stream().self) { + if (data.gainNode) { + data.gainNode.gain.value = val / 100; + } + } else { + vidEl[0].volume = val / 100; + } + const ico = vol.find('a'); + if (val > 0 && ico.hasClass('volume-off')) { + ico.toggleClass('volume-off volume-on'); + video.handleMicStatus(true); + } else if (val === 0 && ico.hasClass('volume-on')) { + ico.toggleClass('volume-on volume-off'); + video.handleMicStatus(false); + } + } + function _mute(mute) { + if (!slider) { + return; + } + muted = mute; + if (mute) { + const val = slider.slider('option', 'value'); + if (val > 0) { + lastVolume = val; + } + slider.slider('option', 'value', 0); + _handle(0); + } else { + slider.slider('option', 'value', lastVolume); + _handle(lastVolume); + } + } + function _destroy() { + if (vol) { + vol.remove(); + vol = null; + } + } + + return { + create: _create + , handle: _handle + , mute: _mute + , muted: function() { + return muted; + } + , destroy: _destroy + }; +}); diff --git a/openmeetings-web/src/main/front/settings/package.json b/openmeetings-web/src/main/front/settings/package.json new file mode 100644 index 0000000..44150d8 --- /dev/null +++ b/openmeetings-web/src/main/front/settings/package.json @@ -0,0 +1,22 @@ +{ + "name": "settings", + "version": "1.0.0", + "description": "Video Utilities and video Settings dialog", + "main": "src/index.js", + "scripts": { + "build-dev": "browserify src/index.js --transform-key=staging -o ${outDir}${npm_package_name}.js", + "build-prod": "browserify src/index.js --transform-key=production -p tinyify -o ${outDir}${npm_package_name}.min.js", + "build": "npm run build-dev && npm run build-prod" + }, + "author": "", + "license": "Apache-2.0", + "rat-license": "Licensed under the Apache License, Version 2.0 (the \"License\") http://www.apache.org/licenses/LICENSE-2.0", + "devDependencies": { + "browserify": "^17.0.0", + "tinyify": "^3.0.0" + }, + "dependencies": { + "adapterjs": "^0.15.5", + "kurento-utils": "^6.15.0" + } +} diff --git a/openmeetings-web/src/main/front/settings/src/index.js b/openmeetings-web/src/main/front/settings/src/index.js new file mode 100644 index 0000000..a0f9250 --- /dev/null +++ b/openmeetings-web/src/main/front/settings/src/index.js @@ -0,0 +1,15 @@ +/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */ +const VideoUtil = require('./video-util'); + +if (window.hasOwnProperty('isSecureContext') === false) { + window.isSecureContext = window.location.protocol == 'https:' || ["localhost", "127.0.0.1"].indexOf(window.location.hostname) !== -1; +} + +Object.assign(window, { + VideoUtil: VideoUtil + , VIDWIN_SEL: VideoUtil.VIDWIN_SEL + , VID_SEL: VideoUtil.VID_SEL + , MicLevel: require('./mic-level') + , kurentoUtils: require('kurento-utils') + , uuidv4: require('uuid/v4') +}); diff --git a/openmeetings-web/src/main/front/settings/src/mic-level.js b/openmeetings-web/src/main/front/settings/src/mic-level.js new file mode 100644 index 0000000..32668f6 --- /dev/null +++ b/openmeetings-web/src/main/front/settings/src/mic-level.js @@ -0,0 +1,91 @@ +/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */ +const RingBuffer = require('./ring-buffer'); + +module.exports = class MicLevel { + constructor() { + let ctx, mic, analyser, vol = .0, vals = new RingBuffer(100); + + this.meterPeer = (rtcPeer, cnvs, _micActivity, _error, connectAudio) => { + if (!rtcPeer || ('function' !== typeof(rtcPeer.getLocalStream) && 'function' !== typeof(rtcPeer.getRemoteStream))) { + return; + } + const stream = rtcPeer.getLocalStream() || rtcPeer.getRemoteStream(); + if (!stream || stream.getAudioTracks().length < 1) { + return; + } + try { + const AudioCtx = window.AudioContext || window.webkitAudioContext; + if (!AudioCtx) { + _error("AudioContext is inaccessible"); + return; + } + ctx = new AudioCtx(); + analyser = ctx.createAnalyser(); + mic = ctx.createMediaStreamSource(stream); + mic.connect(analyser); + if (connectAudio) { + analyser.connect(ctx.destination); + } + this.meter(analyser, cnvs, _micActivity, _error); + } catch (err) { + _error(err); + } + }; + this.meter = (_analyser, cnvs, _micActivity, _error) => { + try { + analyser = _analyser; + analyser.minDecibels = -90; + analyser.maxDecibels = -10; + analyser.fftSize = 256; + const canvas = cnvs[0] + , color = $('body').css('--level-color') + , canvasCtx = canvas.getContext('2d') + , al = analyser.frequencyBinCount + , arr = new Uint8Array(al) + , horiz = cnvs.data('orientation') === 'horizontal'; + function update() { + const WIDTH = canvas.width + , HEIGHT = canvas.height; + canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); + if (!!analyser && cnvs.length > 0) { + if (cnvs.is(':visible')) { + analyser.getByteFrequencyData(arr); + let favg = 0.0; + for (let i = 0; i < al; ++i) { + favg += arr[i] * arr[i]; + } + vol = Math.sqrt(favg / al); + vals.push(vol); + const min = vals.min(); + _micActivity(vol > min + 5); // magic number + canvasCtx.fillStyle = color; + if (horiz) { + canvasCtx.fillRect(0, 0, WIDTH * vol / 100, HEIGHT); + } else { + const h = HEIGHT * vol / 100; + canvasCtx.fillRect(0, HEIGHT - h, WIDTH, h); + } + } + requestAnimationFrame(update); + } + } + update(); + } catch (err) { + _error(err); + } + }; + this.dispose = () => { + if (!!ctx) { + VideoUtil.cleanStream(mic.mediaStream); + VideoUtil.disconnect(mic); + VideoUtil.disconnect(ctx.destination); + ctx.close(); + ctx = null; + } + if (!!analyser) { + VideoUtil.disconnect(analyser); + analyser = null; + } + }; + } +}; diff --git a/openmeetings-web/src/main/front/settings/src/ring-buffer.js b/openmeetings-web/src/main/front/settings/src/ring-buffer.js new file mode 100644 index 0000000..5c10717 --- /dev/null +++ b/openmeetings-web/src/main/front/settings/src/ring-buffer.js @@ -0,0 +1,18 @@ +/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */ +module.exports = class RingBuffer { + constructor(length) { + const buffer = []; + let pos = 0; + + this.get = (key) => { + return buffer[key]; + }; + this.push = (item) => { + buffer[pos] = item; + pos = (pos + 1) % length; + }; + this.min = () => { + return Math.min.apply(Math, buffer); + } + } +}; diff --git a/openmeetings-web/src/main/front/settings/src/settings.js b/openmeetings-web/src/main/front/settings/src/settings.js new file mode 100644 index 0000000..391ca00 --- /dev/null +++ b/openmeetings-web/src/main/front/settings/src/settings.js @@ -0,0 +1,497 @@ +/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */ +const MicLevel = require('./mic-level'); +const VideoUtil = require('./video-util'); +const kurentoUtils = require('kurento-utils'); + +const DEV_AUDIO = 'audioinput', DEV_VIDEO = 'videoinput'; +let vs, lm, s, cam, mic, res, o, rtcPeer, timer + , vidScroll, vid, recBtn, playBtn, recAllowed = false + , level; +const MsgBase = {type: 'kurento', mode: 'test'}; +function _load() { + s = Settings.load(); + if (!s.video) { + const _res = $('#video-settings .cam-resolution option:selected').data(); + s.video = { + cam: 0 + , mic: 0 + , width: _res.width + , height: _res.height + }; + } + return s; +} +function _save() { + Settings.save(s); + OmUtil.sendMessage({ + type: 'av' + , area: 'room' + , settings: s + }); +} +function _clear(_ms) { + const ms = _ms || (vid && vid.length === 1 ? vid[0].srcObject : null); + VideoUtil.cleanStream(ms); + if (vid && vid.length === 1) { + vid[0].srcObject = null; + } + VideoUtil.cleanPeer(rtcPeer); + if (!!lm) { + lm.hide(); + } + if (!!level) { + level.dispose(); + level = null; + } +} +function _close() { + _clear(); + Wicket.Event.unsubscribe('/websocket/message', _onWsMessage); +} +function _onIceCandidate(candidate) { + OmUtil.log('Local candidate' + JSON.stringify(candidate)); + OmUtil.sendMessage({ + id : 'iceCandidate' + , candidate: candidate + }, MsgBase); +} +function _init(options) { + o = JSON.parse(JSON.stringify(options)); + if (!!o.infoMsg) { + OmUtil.alert('info', o.infoMsg, 0); + } + vs = $('#video-settings'); + lm = vs.find('.level-meter'); + cam = vs.find('select.cam').change(function() { + _readValues(); + }); + mic = vs.find('select.mic').change(function() { + _readValues(); + }); + res = vs.find('select.cam-resolution').change(function() { + _readValues(); + }); + vidScroll = vs.find('.vid-block .video-conainer'); + timer = vs.find('.timer'); + vid = vidScroll.find('video'); + recBtn = vs.find('.rec-start') + .click(function() { + recBtn.prop('disabled', true); + _setEnabled(true); + OmUtil.sendMessage({ + id : 'wannaRecord' + }, MsgBase); + }); + playBtn = vs.find('.play') + .click(function() { + recBtn.prop('disabled', true); + _setEnabled(true); + OmUtil.sendMessage({ + id : 'wannaPlay' + }, MsgBase); + }); + vs.find('.btn-save').off().click(function() { + _save(); + _close(); + vs.modal("hide"); + }); + vs.find('.btn-cancel').off().click(function() { + _close(); + vs.modal("hide"); + }); + vs.off().on('hidden.bs.modal', function () { + _close(); + }); + o.width = 300; + o.height = 200; + o.mode = 'settings'; + o.rights = (o.rights || []).join(); + delete o.keycode; + vs.find('.modal-body input, .modal-body button').prop('disabled', true); + const rr = vs.find('.cam-resolution').parents('.sett-row'); + if (!o.interview) { + rr.show(); + } else { + rr.hide(); + } + _load(); + _save(); // trigger settings update +} +function _updateRec() { + recBtn.prop('disabled', !recAllowed || (s.video.cam < 0 && s.video.mic < 0)); +} +function _setCntsDimensions(cnts) { + const b = VideoUtil.browser; + if (b.name === 'Safari') { + let width = s.video.width; + //valid widths are 320, 640, 1280 + [320, 640, 1280].some(function(w) { + if (width < w + 1) { + width = w; + return true; + } + return false; + }); + cnts.video.width = width < 1281 ? width : 1280; + } else { + cnts.video.width = o.interview ? 320 : s.video.width; + cnts.video.height = o.interview ? 260 : s.video.height; + } +} +//each bool OR https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints +// min/ideal/max/exact/mandatory can also be used +function _constraints(sd, callback) { + _getDevConstraints(function(devCnts){ + const cnts = {}; + if (devCnts.video && false === o.audioOnly && VideoUtil.hasCam(sd) && s.video.cam > -1) { + cnts.video = { + frameRate: o.camera.fps + }; + _setCntsDimensions(cnts) + if (!!s.video.camDevice) { + cnts.video.deviceId = { + ideal: s.video.camDevice + }; + } else { + cnts.video.facingMode = { + ideal: 'user' + } + } + } else { + cnts.video = false; + } + if (devCnts.audio && VideoUtil.hasMic(sd) && s.video.mic > -1) { + cnts.audio = { + sampleRate: o.microphone.rate + , echoCancellation: o.microphone.echo + , noiseSuppression: o.microphone.noise + }; + if (!!s.video.micDevice) { + cnts.audio.deviceId = { + ideal: s.video.micDevice + }; + } + } else { + cnts.audio = false; + } + callback(cnts); + }); +} +function _readValues(msg, func) { + const v = cam.find('option:selected') + , m = mic.find('option:selected') + , o = res.find('option:selected').data(); + s.video.cam = 1 * cam.val(); + s.video.camDevice = v.data('device-id'); + s.video.mic = 1 * mic.val(); + s.video.micDevice = m.data('device-id'); + s.video.width = o.width; + s.video.height = o.height; + vid.width(o.width).height(o.height); + vidScroll.scrollLeft(Math.max(0, s.video.width / 2 - 150)) + .scrollTop(Math.max(0, s.video.height / 2 - 110)); + _clear(); + _constraints(null, function(cnts) { + if (cnts.video !== false || cnts.audio !== false) { + const options = VideoUtil.addIceServers({ + localVideo: vid[0] + , mediaConstraints: cnts + }, msg); + rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly( + options + , function(error) { + if (error) { + if (true === rtcPeer.cleaned) { + return; + } + return OmUtil.error(error); + } + if (cnts.audio) { + lm.show(); + level = new MicLevel(); + level.meterPeer(rtcPeer, lm, function(){}, OmUtil.error, false); + } else { + lm.hide(); + } + rtcPeer.generateOffer(function(error, _offerSdp) { + if (error) { + if (true === rtcPeer.cleaned) { + return; + } + return OmUtil.error('Error generating the offer'); + } + if (typeof(func) === 'function') { + func(_offerSdp, cnts); + } else { + _allowRec(true); + } + }); + }); + } + if (!msg) { + _updateRec(); + } + }); +} + +function _allowRec(allow) { + recAllowed = allow; + _updateRec(); +} +function _setLoading(el) { + el.find('option').remove(); + el.append(OmUtil.tmpl('#settings-option-loading')); +} +function _setDisabled(els) { + els.forEach(function(el) { + el.find('option').remove(); + el.append(OmUtil.tmpl('#settings-option-disabled')); + }); +} +function _setSelectedDevice(dev, devIdx) { + let o = dev.find('option[value="' + devIdx + '"]'); + if (o.length === 0 && devIdx !== -1) { + o = dev.find('option[value="0"]'); + } + o.prop('selected', true); +} +function _getDevConstraints(callback) { + const devCnts = {audio: false, video: false, devices: []}; + if (window.isSecureContext === false) { + OmUtil.error($('#settings-https-required').text()); + return; + } + if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { + OmUtil.error('enumerateDevices() not supported.'); + return; + } + navigator.mediaDevices.enumerateDevices() + .then(devices => devices.forEach(device => { + if (DEV_AUDIO === device.kind || DEV_VIDEO === device.kind) { + devCnts.devices.push({ + kind: device.kind + , label: device.label || (device.kind + ' ' + devCnts.devices.length) + , deviceId: device.deviceId + }); + } + if (DEV_AUDIO === device.kind) { + devCnts.audio = true; + } else if (DEV_VIDEO === device.kind) { + devCnts.video = true; + } + })) + .catch(() => OmUtil.error('Unable to get the list of multimedia devices')) + .finally(() => callback(devCnts)); +} +function _initDevices() { + if (window.isSecureContext === false) { + OmUtil.error($('#settings-https-required').text()); + return; + } + if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { + OmUtil.error('enumerateDevices() not supported.'); + return; + } + _setLoading(cam); + _setLoading(mic); + _getDevConstraints(function(devCnts) { + if (!devCnts.audio && !devCnts.video) { + _setDisabled([cam, mic]); + return; + } + navigator.mediaDevices.getUserMedia(devCnts) + .then(stream => { + const devices = navigator.mediaDevices.enumerateDevices() + .catch(function(err) { + throw err; + }) + .finally(() => _clear(stream)); + return devices || devCnts.devices; + }) + .catch(function() { + return devCnts.devices; + }) + .then(devices => { + let cCount = 0, mCount = 0; + _load(); + _setDisabled([cam, mic]); + devices.forEach(device => { + if (DEV_AUDIO === device.kind) { + const o = $('<option></option>').attr('value', mCount).text(device.label) + .data('device-id', device.deviceId); + mic.append(o); + mCount++; + } else if (DEV_VIDEO === device.kind) { + const o = $('<option></option>').attr('value', cCount).text(device.label) + .data('device-id', device.deviceId); + cam.append(o); + cCount++; + } + }); + _setSelectedDevice(cam, s.video.cam); + _setSelectedDevice(mic, s.video.mic); + res.find('option').each(function() { + const o = $(this).data(); + if (o.width === s.video.width && o.height === s.video.height) { + $(this).prop('selected', true); + return false; + } + }); + _readValues(); + }) + .catch(function(err) { + _setDisabled([cam, mic]); + OmUtil.error(err); + }); + }); +} +function _open() { + Wicket.Event.subscribe('/websocket/message', _onWsMessage); + recAllowed = false; + timer.hide(); + playBtn.prop('disabled', true); + vs.modal('show'); + _load(); + _initDevices(); +} +function _setEnabled(enabled) { + playBtn.prop('disabled', enabled); + cam.prop('disabled', enabled); + mic.prop('disabled', enabled); + res.prop('disabled', enabled); +} +function _onStop() { + _updateRec(); + _setEnabled(false); +} +function _onKMessage(m) { + OmUtil.info('Received message: ', m); + switch (m.id) { + case 'canRecord': + _readValues(m, function(_offerSdp, cnts) { + OmUtil.info('Invoking SDP offer callback function'); + OmUtil.sendMessage({ + id : 'record' + , sdpOffer: _offerSdp + , video: cnts.video !== false + , audio: cnts.audio !== false + }, MsgBase); + rtcPeer.on('icecandidate', _onIceCandidate); + }); + break; + case 'canPlay': + { + const options = VideoUtil.addIceServers({ + remoteVideo: vid[0] + , mediaConstraints: {audio: true, video: true} + , onicecandidate: _onIceCandidate + }, m); + _clear(); + rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly( + options + , function(error) { + if (error) { + if (true === rtcPeer.cleaned) { + return; + } + return OmUtil.error(error); + } + rtcPeer.generateOffer(function(error, offerSdp) { + if (error) { + if (true === rtcPeer.cleaned) { + return; + } + return OmUtil.error('Error generating the offer'); + } + OmUtil.sendMessage({ + id : 'play' + , sdpOffer: offerSdp + }, MsgBase); + }); + }); + } + break; + case 'playResponse': + OmUtil.log('Play SDP answer received from server. Processing ...'); + rtcPeer.processAnswer(m.sdpAnswer, function(error) { + if (error) { + if (true === rtcPeer.cleaned) { + return; + } + return OmUtil.error(error); + } + lm.show(); + level = new MicLevel(); + level.meterPeer(rtcPeer, lm, function(){}, OmUtil.error, true); + }); + break; + case 'startResponse': + OmUtil.log('SDP answer received from server. Processing ...'); + rtcPeer.processAnswer(m.sdpAnswer, function(error) { + if (error) { + if (true === rtcPeer.cleaned) { + return; + } + return OmUtil.error(error); + } + }); + break; + case 'iceCandidate': + rtcPeer.addIceCandidate(m.candidate, function(error) { + if (error) { + if (true === rtcPeer.cleaned) { + return; + } + return OmUtil.error('Error adding candidate: ' + error); + } + }); + break; + case 'recording': + timer.show().find('.time').text(m.time); + break; + case 'recStopped': + timer.hide(); + _onStop(); + break; + case 'playStopped': + _onStop(); + _readValues(); + break; + default: + // no-op + } +} +function _onWsMessage(jqEvent, msg) { + try { + if (msg instanceof Blob) { + return; //ping + } + const m = JSON.parse(msg); + if (m && 'kurento' === m.type) { + if ('test' === m.mode) { + _onKMessage(m); + } + switch (m.id) { + case 'error': + OmUtil.error(m.message); + break; + default: + //no-op + } + } + } catch (err) { + OmUtil.error(err); + } +} + +module.exports = { + init: _init + , open: _open + , close: function() { + _close(); + vs && vs.modal('hide'); + } + , load: _load + , save: _save + , constraints: _constraints +}; diff --git a/openmeetings-web/src/main/front/settings/src/video-util.js b/openmeetings-web/src/main/front/settings/src/video-util.js new file mode 100644 index 0000000..c2f983a --- /dev/null +++ b/openmeetings-web/src/main/front/settings/src/video-util.js @@ -0,0 +1,333 @@ +/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */ +const WB_AREA_SEL = '.room-block .wb-block'; +const WBA_WB_SEL = '.room-block .wb-block .wb-tab-content'; +const VIDWIN_SEL = '.video.user-video'; +const VID_SEL = '.video-container[id!=user-video]'; +const CAM_ACTIVITY = 'VIDEO'; +const MIC_ACTIVITY = 'AUDIO'; +const SCREEN_ACTIVITY = 'SCREEN'; +const REC_ACTIVITY = 'RECORD'; + +const UAParser = require('ua-parser-js') + , ua = (typeof window !== 'undefined' && window.navigator) ? window.navigator.userAgent : '' + , parser = new UAParser(ua) + , browser = parser.getBrowser(); + +function _getVid(uid) { + return 'video' + uid; +} +function _isSharing(sd) { + return !!sd && 'SCREEN' === sd.type && sd.activities.includes(SCREEN_ACTIVITY); +} +function _isRecording(sd) { + return !!sd && 'SCREEN' === sd.type && sd.activities.includes(REC_ACTIVITY); +} +function _hasMic(sd) { + return !sd || sd.activities.includes(MIC_ACTIVITY); +} +function _hasCam(sd) { + return !sd || sd.activities.includes(CAM_ACTIVITY); +} +function _hasVideo(sd) { + return _hasCam(sd) || _isSharing(sd) || _isRecording(sd); +} +function _getRects(sel, excl) { + const list = [], elems = $(sel); + for (let i = 0; i < elems.length; ++i) { + if (excl !== $(elems[i]).attr('aria-describedby')) { + list.push(_getRect(elems[i])); + } + } + return list; +} +function _getRect(e) { + const win = $(e), winoff = win.offset(); + return {left: winoff.left + , top: winoff.top + , right: winoff.left + win.width() + , bottom: winoff.top + win.height()}; +} +function _container() { + const a = $(WB_AREA_SEL); + const c = a.find('.wb-area .tabs .wb-tab-content'); + return c.length > 0 ? $(WBA_WB_SEL) : a; +} +function __processTopToBottom(area, rectNew, list) { + const offsetX = 20 + , offsetY = 10; + + let minY = area.bottom, posFound; + do { + posFound = true; + for (let i = 0; i < list.length; ++i) { + const rect = list[i]; + minY = Math.min(minY, rect.bottom); + + if (rectNew.left < rect.right && rectNew.right > rect.left && rectNew.top < rect.bottom && rectNew.bottom > rect.top) { + rectNew.left = rect.right + offsetX; + posFound = false; + } + if (rectNew.right >= area.right) { + rectNew.left = area.left; + rectNew.top = Math.max(minY, rectNew.top) + offsetY; + posFound = false; + } + if (rectNew.bottom >= area.bottom) { + rectNew.top = area.top; + posFound = true; + break; + } + } + } while (!posFound); + return {left: rectNew.left, top: rectNew.top}; +} +function __processEqualsBottomToTop(area, rectNew, list) { + const offsetX = 20 + , offsetY = 10; + + rectNew.bottom = area.bottom; + let minY = area.bottom, posFound; + do { + posFound = true; + for (let i = 0; i < list.length; ++i) { + const rect = list[i]; + minY = Math.min(minY, rect.top); + + if (rectNew.left < rect.right && rectNew.right > rect.left && rectNew.top < rect.bottom && rectNew.bottom > rect.top) { + rectNew.left = rect.right + offsetX; + posFound = false; + } + if (rectNew.right >= area.right) { + rectNew.left = area.left; + rectNew.bottom = Math.min(minY, rectNew.top) - offsetY; + posFound = false; + } + if (rectNew.top <= area.top) { + rectNew.top = area.top; + posFound = true; + break; + } + } + } while (!posFound); + return {left: rectNew.left, top: rectNew.top}; +} +function _getPos(list, w, h, _processor) { + if (Room.getOptions().interview) { + return {left: 0, top: 0}; + } + const wba = _container() + , woffset = wba.offset() + , area = {left: woffset.left, top: woffset.top, right: woffset.left + wba.width(), bottom: woffset.top + wba.height()} + , rectNew = { + _left: area.left + , _top: area.top + , _right: area.left + w + , _bottom: area.top + h + , get left() { + return this._left; + } + , set left(l) { + this._left = l; + this._right = l + w; + } + , get right() { + return this._right; + } + , get top() { + return this._top; + } + , set top(t) { + this._top = t; + this._bottom = t + h; + } + , set bottom(b) { + this._bottom = b; + this._top = b - h; + } + , get bottom() { + return this._bottom; + } + }; + const processor = _processor || __processTopToBottom; + return processor(area, rectNew, list); +} +function _arrange() { + const list = []; + $(VIDWIN_SEL).each(function() { + const v = $(this); + v.css(_getPos(list, v.width(), v.height())); + list.push(_getRect(v)); + }); +} +function _arrangeResize() { + const list = []; + function __getDialog(_v) { + return $(_v).find('.video-container.ui-dialog-content'); + } + $(VIDWIN_SEL).toArray().sort((v1, v2) => { + const c1 = __getDialog(v1).data().stream() + , c2 = __getDialog(v2).data().stream(); + return c2.level - c1.level || c1.user.displayName.localeCompare(c2.user.displayName); + }).forEach(_v => { + const v = $(_v); + __getDialog(v) + .dialog('option', 'width', 120) + .dialog('option', 'height', 90); + v.css(_getPos(list, v.width(), v.height(), __processEqualsBottomToTop)); + list.push(_getRect(v)); + }); +} +function _cleanStream(stream) { + if (!!stream) { + stream.getTracks().forEach(track => track.stop()); + } +} +function _cleanPeer(peer) { + if (!!peer) { + peer.cleaned = true; + try { + const pc = peer.peerConnection; + if (!!pc) { + pc.getSenders().forEach(sender => { + try { + if (sender.track) { + sender.track.stop(); + } + } catch(e) { + OmUtil.log('Failed to clean sender' + e); + } + }); + pc.getReceivers().forEach(receiver => { + try { + if (receiver.track) { + receiver.track.stop(); + } + } catch(e) { + OmUtil.log('Failed to clean receiver' + e); + } + }); + pc.onconnectionstatechange = null; + pc.ontrack = null; + pc.onremovetrack = null; + pc.onremovestream = null; + pc.onicecandidate = null; + pc.oniceconnectionstatechange = null; + pc.onsignalingstatechange = null; + pc.onicegatheringstatechange = null; + pc.onnegotiationneeded = null; + } + peer.dispose(); + peer.removeAllListeners('icecandidate'); + delete peer.generateOffer; + delete peer.processAnswer; + delete peer.processOffer; + delete peer.addIceCandidate; + } catch(e) { + //no-op + } + } +} +function _isChrome(_b) { + const b = _b || browser; + return b.name === 'Chrome' || b.name === 'Chromium'; +} +function _isEdge(_b) { + const b = _b || browser; + return b.name === 'Edge' && "MSGestureEvent" in window; +} +function _isEdgeChromium(_b) { + const b = _b || browser; + return b.name === 'Edge' && !("MSGestureEvent" in window); +} +function _setPos(v, pos) { + if (v.dialog('instance')) { + v.dialog('widget').css(pos); + } +} +function _askPermission(callback) { + const perm = $('#ask-permission'); + if (undefined === perm.dialog('instance')) { + perm.data('callbacks', []).dialog({ + appendTo: '.room-block .room-container' + , dialogClass: "ask-video-play-permission" + , autoOpen: true + , buttons: [ + { + text: perm.data('btn-ok') + , click: function() { + while (perm.data('callbacks').length > 0) { + perm.data('callbacks').pop()(); + } + $(this).dialog('close'); + } + } + ] + }); + } else if (!perm.dialog('isOpen')) { + perm.dialog('open') + } + perm.data('callbacks').push(callback); +} +function _disconnect(node) { + try { + node.disconnect(); //this one can throw + } catch (e) { + //no-op + } +} +function _sharingSupported() { + const b = browser; + return (b.name === 'Edge' && b.major > 16) + || (b.name === 'Firefox') + || (b.name === 'Opera') + || (b.name === 'Yandex') + || _isChrome(b) + || _isEdgeChromium(b) + || (b.name === 'Mozilla' && b.major > 4); +} +function _highlight(el, clazz, count) { + if (!el || el.length < 1 || el.hasClass('disabled') || count < 0) { + return; + } + el.addClass(clazz).delay(2000).queue(function(next) { + el.removeClass(clazz).delay(2000).queue(function(next1) { + _highlight(el, clazz, --count); + next1(); + }); + next(); + }); +} + +module.exports = { + VIDWIN_SEL: VIDWIN_SEL + , VID_SEL: VID_SEL + + , browser: browser + , getVid: _getVid + , isSharing: _isSharing + , isRecording: _isRecording + , hasMic: _hasMic + , hasCam: _hasCam + , hasVideo: _hasVideo + , getRects: _getRects + , getPos: _getPos + , container: _container + , arrange: _arrange + , arrangeResize: _arrangeResize + , cleanStream: _cleanStream + , cleanPeer: _cleanPeer + , addIceServers: function(opts, m) { + if (m && m.iceServers && m.iceServers.length > 0) { + opts.configuration = {iceServers: m.iceServers}; + } + return opts; + } + , isEdge: _isEdge + , isEdgeChromium: _isEdgeChromium + , isChrome: _isChrome + , setPos: _setPos + , askPermission: _askPermission + , disconnect: _disconnect + , sharingSupported: _sharingSupported + , highlight: _highlight +}; diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-settings.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-settings.js deleted file mode 100644 index 1019f4f..0000000 --- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-settings.js +++ /dev/null @@ -1,605 +0,0 @@ -/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */ -if (window.hasOwnProperty('isSecureContext') === false) { - window.isSecureContext = window.location.protocol == 'https:' || ["localhost", "127.0.0.1"].indexOf(window.location.hostname) !== -1; -} -var RingBuffer = (function(length) { - const buffer = []; - let pos = 0; - - return { - get: function(key){ - return buffer[key]; - } - , push: function(item) { - buffer[pos] = item; - pos = (pos + 1) % length; - } - , min: function(){ - return Math.min.apply(Math, buffer); - } - }; -}); -var MicLevel = (function() { - let ctx, mic, analyser, vol = .0, vals = RingBuffer(100); - - function _meterPeer(rtcPeer, cnvs, _micActivity, _error, connectAudio) { - if (!rtcPeer || ('function' !== typeof(rtcPeer.getLocalStream) && 'function' !== typeof(rtcPeer.getRemoteStream))) { - return; - } - const stream = rtcPeer.getLocalStream() || rtcPeer.getRemoteStream(); - if (!stream || stream.getAudioTracks().length < 1) { - return; - } - try { - const AudioCtx = window.AudioContext || window.webkitAudioContext; - if (!AudioCtx) { - _error("AudioContext is inaccessible"); - return; - } - ctx = new AudioCtx(); - analyser = ctx.createAnalyser(); - mic = ctx.createMediaStreamSource(stream); - mic.connect(analyser); - if (connectAudio) { - analyser.connect(ctx.destination); - } - _meter(analyser, cnvs, _micActivity, _error); - } catch (err) { - _error(err); - } - } - function _meter(_analyser, cnvs, _micActivity, _error) { - try { - analyser = _analyser; - analyser.minDecibels = -90; - analyser.maxDecibels = -10; - analyser.fftSize = 256; - const canvas = cnvs[0] - , color = $('body').css('--level-color') - , canvasCtx = canvas.getContext('2d') - , al = analyser.frequencyBinCount - , arr = new Uint8Array(al) - , horiz = cnvs.data('orientation') === 'horizontal'; - function update() { - const WIDTH = canvas.width - , HEIGHT = canvas.height; - canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); - if (!!analyser && cnvs.length > 0) { - if (cnvs.is(':visible')) { - analyser.getByteFrequencyData(arr); - let favg = 0.0; - for (let i = 0; i < al; ++i) { - favg += arr[i] * arr[i]; - } - vol = Math.sqrt(favg / al); - vals.push(vol); - const min = vals.min(); - _micActivity(vol > min + 5); // magic number - canvasCtx.fillStyle = color; - if (horiz) { - canvasCtx.fillRect(0, 0, WIDTH * vol / 100, HEIGHT); - } else { - const h = HEIGHT * vol / 100; - canvasCtx.fillRect(0, HEIGHT - h, WIDTH, h); - } - } - requestAnimationFrame(update); - } - } - update(); - } catch (err) { - _error(err); - } - } - function _dispose() { - if (!!ctx) { - VideoUtil.cleanStream(mic.mediaStream); - VideoUtil.disconnect(mic); - VideoUtil.disconnect(ctx.destination); - ctx.close(); - ctx = null; - } - if (!!analyser) { - VideoUtil.disconnect(analyser); - analyser = null; - } - } - return { - meter: _meter - , meterPeer: _meterPeer - , dispose: _dispose - }; -}); -var VideoSettings = (function() { - const DEV_AUDIO = 'audioinput', DEV_VIDEO = 'videoinput'; - let vs, lm, s, cam, mic, res, o, rtcPeer, timer - , vidScroll, vid, recBtn, playBtn, recAllowed = false - , level; - const MsgBase = {type: 'kurento', mode: 'test'}; - function _load() { - s = Settings.load(); - if (!s.video) { - const _res = $('#video-settings .cam-resolution option:selected').data(); - s.video = { - cam: 0 - , mic: 0 - , width: _res.width - , height: _res.height - }; - } - return s; - } - function _save() { - Settings.save(s); - OmUtil.sendMessage({ - type: 'av' - , area: 'room' - , settings: s - }); - } - function _clear(_ms) { - const ms = _ms || (vid && vid.length === 1 ? vid[0].srcObject : null); - VideoUtil.cleanStream(ms); - if (vid && vid.length === 1) { - vid[0].srcObject = null; - } - VideoUtil.cleanPeer(rtcPeer); - if (!!lm) { - lm.hide(); - } - if (!!level) { - level.dispose(); - level = null; - } - } - function _close() { - _clear(); - Wicket.Event.unsubscribe('/websocket/message', _onWsMessage); - } - function _onIceCandidate(candidate) { - OmUtil.log('Local candidate' + JSON.stringify(candidate)); - OmUtil.sendMessage({ - id : 'iceCandidate' - , candidate: candidate - }, MsgBase); - } - function _init(options) { - o = JSON.parse(JSON.stringify(options)); - if (!!o.infoMsg) { - OmUtil.alert('info', o.infoMsg, 0); - } - vs = $('#video-settings'); - lm = vs.find('.level-meter'); - cam = vs.find('select.cam').change(function() { - _readValues(); - }); - mic = vs.find('select.mic').change(function() { - _readValues(); - }); - res = vs.find('select.cam-resolution').change(function() { - _readValues(); - }); - vidScroll = vs.find('.vid-block .video-conainer'); - timer = vs.find('.timer'); - vid = vidScroll.find('video'); - recBtn = vs.find('.rec-start') - .click(function() { - recBtn.prop('disabled', true); - _setEnabled(true); - OmUtil.sendMessage({ - id : 'wannaRecord' - }, MsgBase); - }); - playBtn = vs.find('.play') - .click(function() { - recBtn.prop('disabled', true); - _setEnabled(true); - OmUtil.sendMessage({ - id : 'wannaPlay' - }, MsgBase); - }); - vs.find('.btn-save').off().click(function() { - _save(); - _close(); - vs.modal("hide"); - }); - vs.find('.btn-cancel').off().click(function() { - _close(); - vs.modal("hide"); - }); - vs.off().on('hidden.bs.modal', function () { - _close(); - }); - o.width = 300; - o.height = 200; - o.mode = 'settings'; - o.rights = (o.rights || []).join(); - delete o.keycode; - vs.find('.modal-body input, .modal-body button').prop('disabled', true); - const rr = vs.find('.cam-resolution').parents('.sett-row'); - if (!o.interview) { - rr.show(); - } else { - rr.hide(); - } - _load(); - _save(); // trigger settings update - } - function _updateRec() { - recBtn.prop('disabled', !recAllowed || (s.video.cam < 0 && s.video.mic < 0)); - } - function _setCntsDimensions(cnts) { - const b = kurentoUtils.WebRtcPeer.browser; - if (b.name === 'Safari') { - let width = s.video.width; - //valid widths are 320, 640, 1280 - [320, 640, 1280].some(function(w) { - if (width < w + 1) { - width = w; - return true; - } - return false; - }); - cnts.video.width = width < 1281 ? width : 1280; - } else { - cnts.video.width = o.interview ? 320 : s.video.width; - cnts.video.height = o.interview ? 260 : s.video.height; - } - } - //each bool OR https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints - // min/ideal/max/exact/mandatory can also be used - function _constraints(sd, callback) { - _getDevConstraints(function(devCnts){ - const cnts = {}; - if (devCnts.video && false === o.audioOnly && VideoUtil.hasCam(sd) && s.video.cam > -1) { - cnts.video = { - frameRate: o.camera.fps - }; - _setCntsDimensions(cnts) - if (!!s.video.camDevice) { - cnts.video.deviceId = { - ideal: s.video.camDevice - }; - } else { - cnts.video.facingMode = { - ideal: 'user' - } - } - } else { - cnts.video = false; - } - if (devCnts.audio && VideoUtil.hasMic(sd) && s.video.mic > -1) { - cnts.audio = { - sampleRate: o.microphone.rate - , echoCancellation: o.microphone.echo - , noiseSuppression: o.microphone.noise - }; - if (!!s.video.micDevice) { - cnts.audio.deviceId = { - ideal: s.video.micDevice - }; - } - } else { - cnts.audio = false; - } - callback(cnts); - }); - } - function _readValues(msg, func) { - const v = cam.find('option:selected') - , m = mic.find('option:selected') - , o = res.find('option:selected').data(); - s.video.cam = 1 * cam.val(); - s.video.camDevice = v.data('device-id'); - s.video.mic = 1 * mic.val(); - s.video.micDevice = m.data('device-id'); - s.video.width = o.width; - s.video.height = o.height; - vid.width(o.width).height(o.height); - vidScroll.scrollLeft(Math.max(0, s.video.width / 2 - 150)) - .scrollTop(Math.max(0, s.video.height / 2 - 110)); - _clear(); - _constraints(null, function(cnts) { - if (cnts.video !== false || cnts.audio !== false) { - const options = VideoUtil.addIceServers({ - localVideo: vid[0] - , mediaConstraints: cnts - }, msg); - rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly( - options - , function(error) { - if (error) { - if (true === rtcPeer.cleaned) { - return; - } - return OmUtil.error(error); - } - if (cnts.audio) { - lm.show(); - level = MicLevel(); - level.meterPeer(rtcPeer, lm, function(){}, OmUtil.error, false); - } else { - lm.hide(); - } - rtcPeer.generateOffer(function(error, _offerSdp) { - if (error) { - if (true === rtcPeer.cleaned) { - return; - } - return OmUtil.error('Error generating the offer'); - } - if (typeof(func) === 'function') { - func(_offerSdp, cnts); - } else { - _allowRec(true); - } - }); - }); - } - if (!msg) { - _updateRec(); - } - }); - } - - function _allowRec(allow) { - recAllowed = allow; - _updateRec(); - } - function _setLoading(el) { - el.find('option').remove(); - el.append(OmUtil.tmpl('#settings-option-loading')); - } - function _setDisabled(els) { - els.forEach(function(el) { - el.find('option').remove(); - el.append(OmUtil.tmpl('#settings-option-disabled')); - }); - } - function _setSelectedDevice(dev, devIdx) { - let o = dev.find('option[value="' + devIdx + '"]'); - if (o.length === 0 && devIdx !== -1) { - o = dev.find('option[value="0"]'); - } - o.prop('selected', true); - } - function _getDevConstraints(callback) { - const devCnts = {audio: false, video: false, devices: []}; - if (window.isSecureContext === false) { - OmUtil.error($('#settings-https-required').text()); - return; - } - if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { - OmUtil.error('enumerateDevices() not supported.'); - return; - } - navigator.mediaDevices.enumerateDevices() - .then(devices => devices.forEach(device => { - if (DEV_AUDIO === device.kind || DEV_VIDEO === device.kind) { - devCnts.devices.push({ - kind: device.kind - , label: device.label || (device.kind + ' ' + devCnts.devices.length) - , deviceId: device.deviceId - }); - } - if (DEV_AUDIO === device.kind) { - devCnts.audio = true; - } else if (DEV_VIDEO === device.kind) { - devCnts.video = true; - } - })) - .catch(() => OmUtil.error('Unable to get the list of multimedia devices')) - .finally(() => callback(devCnts)); - } - function _initDevices() { - if (window.isSecureContext === false) { - OmUtil.error($('#settings-https-required').text()); - return; - } - if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { - OmUtil.error('enumerateDevices() not supported.'); - return; - } - _setLoading(cam); - _setLoading(mic); - _getDevConstraints(function(devCnts) { - if (!devCnts.audio && !devCnts.video) { - _setDisabled([cam, mic]); - return; - } - navigator.mediaDevices.getUserMedia(devCnts) - .then(stream => { - const devices = navigator.mediaDevices.enumerateDevices() - .catch(function(err) { - throw err; - }) - .finally(() => _clear(stream)); - return devices || devCnts.devices; - }) - .catch(function() { - return devCnts.devices; - }) - .then(devices => { - let cCount = 0, mCount = 0; - _load(); - _setDisabled([cam, mic]); - devices.forEach(device => { - if (DEV_AUDIO === device.kind) { - const o = $('<option></option>').attr('value', mCount).text(device.label) - .data('device-id', device.deviceId); - mic.append(o); - mCount++; - } else if (DEV_VIDEO === device.kind) { - const o = $('<option></option>').attr('value', cCount).text(device.label) - .data('device-id', device.deviceId); - cam.append(o); - cCount++; - } - }); - _setSelectedDevice(cam, s.video.cam); - _setSelectedDevice(mic, s.video.mic); - res.find('option').each(function() { - const o = $(this).data(); - if (o.width === s.video.width && o.height === s.video.height) { - $(this).prop('selected', true); - return false; - } - }); - _readValues(); - }) - .catch(function(err) { - _setDisabled([cam, mic]); - OmUtil.error(err); - }); - }); - } - function _open() { - Wicket.Event.subscribe('/websocket/message', _onWsMessage); - recAllowed = false; - timer.hide(); - playBtn.prop('disabled', true); - vs.modal('show'); - _load(); - _initDevices(); - } - function _setEnabled(enabled) { - playBtn.prop('disabled', enabled); - cam.prop('disabled', enabled); - mic.prop('disabled', enabled); - res.prop('disabled', enabled); - } - function _onStop() { - _updateRec(); - _setEnabled(false); - } - function _onKMessage(m) { - OmUtil.info('Received message: ', m); - switch (m.id) { - case 'canRecord': - _readValues(m, function(_offerSdp, cnts) { - OmUtil.info('Invoking SDP offer callback function'); - OmUtil.sendMessage({ - id : 'record' - , sdpOffer: _offerSdp - , video: cnts.video !== false - , audio: cnts.audio !== false - }, MsgBase); - rtcPeer.on('icecandidate', _onIceCandidate); - }); - break; - case 'canPlay': - { - const options = VideoUtil.addIceServers({ - remoteVideo: vid[0] - , mediaConstraints: {audio: true, video: true} - , onicecandidate: _onIceCandidate - }, m); - _clear(); - rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly( - options - , function(error) { - if (error) { - if (true === rtcPeer.cleaned) { - return; - } - return OmUtil.error(error); - } - rtcPeer.generateOffer(function(error, offerSdp) { - if (error) { - if (true === rtcPeer.cleaned) { - return; - } - return OmUtil.error('Error generating the offer'); - } - OmUtil.sendMessage({ - id : 'play' - , sdpOffer: offerSdp - }, MsgBase); - }); - }); - } - break; - case 'playResponse': - OmUtil.log('Play SDP answer received from server. Processing ...'); - rtcPeer.processAnswer(m.sdpAnswer, function(error) { - if (error) { - if (true === rtcPeer.cleaned) { - return; - } - return OmUtil.error(error); - } - lm.show(); - level = MicLevel(); - level.meterPeer(rtcPeer, lm, function(){}, OmUtil.error, true); - }); - break; - case 'startResponse': - OmUtil.log('SDP answer received from server. Processing ...'); - rtcPeer.processAnswer(m.sdpAnswer, function(error) { - if (error) { - if (true === rtcPeer.cleaned) { - return; - } - return OmUtil.error(error); - } - }); - break; - case 'iceCandidate': - rtcPeer.addIceCandidate(m.candidate, function(error) { - if (error) { - if (true === rtcPeer.cleaned) { - return; - } - return OmUtil.error('Error adding candidate: ' + error); - } - }); - break; - case 'recording': - timer.show().find('.time').text(m.time); - break; - case 'recStopped': - timer.hide(); - _onStop(); - break; - case 'playStopped': - _onStop(); - _readValues(); - break; - default: - // no-op - } - } - function _onWsMessage(jqEvent, msg) { - try { - if (msg instanceof Blob) { - return; //ping - } - const m = JSON.parse(msg); - if (m && 'kurento' === m.type) { - if ('test' === m.mode) { - _onKMessage(m); - } - switch (m.id) { - case 'error': - OmUtil.error(m.message); - break; - default: - //no-op - } - } - } catch (err) { - OmUtil.error(err); - } - } - return { - init: _init - , open: _open - , close: function() { - _close(); - vs && vs.modal('hide'); - } - , load: _load - , save: _save - , constraints: _constraints - }; -})(); diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video-util.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video-util.js deleted file mode 100644 index 524509f..0000000 --- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video-util.js +++ /dev/null @@ -1,445 +0,0 @@ -/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */ -const WB_AREA_SEL = '.room-block .wb-block'; -const WBA_WB_SEL = '.room-block .wb-block .wb-tab-content'; -const VIDWIN_SEL = '.video.user-video'; -const VID_SEL = '.video-container[id!=user-video]'; -const CAM_ACTIVITY = 'VIDEO'; -const MIC_ACTIVITY = 'AUDIO'; -const SCREEN_ACTIVITY = 'SCREEN'; -const REC_ACTIVITY = 'RECORD'; -var VideoUtil = (function() { - const self = {}; - function _getVid(uid) { - return 'video' + uid; - } - function _isSharing(sd) { - return !!sd && 'SCREEN' === sd.type && sd.activities.includes(SCREEN_ACTIVITY); - } - function _isRecording(sd) { - return !!sd && 'SCREEN' === sd.type && sd.activities.includes(REC_ACTIVITY); - } - function _hasMic(sd) { - return !sd || sd.activities.includes(MIC_ACTIVITY); - } - function _hasCam(sd) { - return !sd || sd.activities.includes(CAM_ACTIVITY); - } - function _hasVideo(sd) { - return _hasCam(sd) || _isSharing(sd) || _isRecording(sd); - } - function _getRects(sel, excl) { - const list = [], elems = $(sel); - for (let i = 0; i < elems.length; ++i) { - if (excl !== $(elems[i]).attr('aria-describedby')) { - list.push(_getRect(elems[i])); - } - } - return list; - } - function _getRect(e) { - const win = $(e), winoff = win.offset(); - return {left: winoff.left - , top: winoff.top - , right: winoff.left + win.width() - , bottom: winoff.top + win.height()}; - } - function _container() { - const a = $(WB_AREA_SEL); - const c = a.find('.wb-area .tabs .wb-tab-content'); - return c.length > 0 ? $(WBA_WB_SEL) : a; - } - function __processTopToBottom(area, rectNew, list) { - const offsetX = 20 - , offsetY = 10; - - let minY = area.bottom, posFound; - do { - posFound = true; - for (let i = 0; i < list.length; ++i) { - const rect = list[i]; - minY = Math.min(minY, rect.bottom); - - if (rectNew.left < rect.right && rectNew.right > rect.left && rectNew.top < rect.bottom && rectNew.bottom > rect.top) { - rectNew.left = rect.right + offsetX; - posFound = false; - } - if (rectNew.right >= area.right) { - rectNew.left = area.left; - rectNew.top = Math.max(minY, rectNew.top) + offsetY; - posFound = false; - } - if (rectNew.bottom >= area.bottom) { - rectNew.top = area.top; - posFound = true; - break; - } - } - } while (!posFound); - return {left: rectNew.left, top: rectNew.top}; - } - function __processEqualsBottomToTop(area, rectNew, list) { - const offsetX = 20 - , offsetY = 10; - - rectNew.bottom = area.bottom; - let minY = area.bottom, posFound; - do { - posFound = true; - for (let i = 0; i < list.length; ++i) { - const rect = list[i]; - minY = Math.min(minY, rect.top); - - if (rectNew.left < rect.right && rectNew.right > rect.left && rectNew.top < rect.bottom && rectNew.bottom > rect.top) { - rectNew.left = rect.right + offsetX; - posFound = false; - } - if (rectNew.right >= area.right) { - rectNew.left = area.left; - rectNew.bottom = Math.min(minY, rectNew.top) - offsetY; - posFound = false; - } - if (rectNew.top <= area.top) { - rectNew.top = area.top; - posFound = true; - break; - } - } - } while (!posFound); - return {left: rectNew.left, top: rectNew.top}; - } - function _getPos(list, w, h, _processor) { - if (Room.getOptions().interview) { - return {left: 0, top: 0}; - } - const wba = _container() - , woffset = wba.offset() - , area = {left: woffset.left, top: woffset.top, right: woffset.left + wba.width(), bottom: woffset.top + wba.height()} - , rectNew = { - _left: area.left - , _top: area.top - , _right: area.left + w - , _bottom: area.top + h - , get left() { - return this._left; - } - , set left(l) { - this._left = l; - this._right = l + w; - } - , get right() { - return this._right; - } - , get top() { - return this._top; - } - , set top(t) { - this._top = t; - this._bottom = t + h; - } - , set bottom(b) { - this._bottom = b; - this._top = b - h; - } - , get bottom() { - return this._bottom; - } - }; - const processor = _processor || __processTopToBottom; - return processor(area, rectNew, list); - } - function _arrange() { - const list = []; - $(VIDWIN_SEL).each(function() { - const v = $(this); - v.css(_getPos(list, v.width(), v.height())); - list.push(_getRect(v)); - }); - } - function _arrangeResize() { - const list = []; - function __getDialog(_v) { - return $(_v).find('.video-container.ui-dialog-content'); - } - $(VIDWIN_SEL).toArray().sort((v1, v2) => { - const c1 = __getDialog(v1).data().stream() - , c2 = __getDialog(v2).data().stream(); - return c2.level - c1.level || c1.user.displayName.localeCompare(c2.user.displayName); - }).forEach(_v => { - const v = $(_v); - __getDialog(v) - .dialog('option', 'width', 120) - .dialog('option', 'height', 90); - v.css(_getPos(list, v.width(), v.height(), __processEqualsBottomToTop)); - list.push(_getRect(v)); - }); - } - function _cleanStream(stream) { - if (!!stream) { - stream.getTracks().forEach(track => track.stop()); - } - } - function _cleanPeer(peer) { - if (!!peer) { - peer.cleaned = true; - try { - const pc = peer.peerConnection; - if (!!pc) { - pc.getSenders().forEach(sender => { - try { - if (sender.track) { - sender.track.stop(); - } - } catch(e) { - OmUtil.log('Failed to clean sender' + e); - } - }); - pc.getReceivers().forEach(receiver => { - try { - if (receiver.track) { - receiver.track.stop(); - } - } catch(e) { - OmUtil.log('Failed to clean receiver' + e); - } - }); - pc.onconnectionstatechange = null; - pc.ontrack = null; - pc.onremovetrack = null; - pc.onremovestream = null; - pc.onicecandidate = null; - pc.oniceconnectionstatechange = null; - pc.onsignalingstatechange = null; - pc.onicegatheringstatechange = null; - pc.onnegotiationneeded = null; - } - peer.dispose(); - peer.removeAllListeners('icecandidate'); - delete peer.generateOffer; - delete peer.processAnswer; - delete peer.processOffer; - delete peer.addIceCandidate; - } catch(e) { - //no-op - } - } - } - function _isChrome(_b) { - const b = _b || kurentoUtils.WebRtcPeer.browser; - return b.name === 'Chrome' || b.name === 'Chromium'; - } - function _isEdge(_b) { - const b = _b || kurentoUtils.WebRtcPeer.browser; - return b.name === 'Edge' && "MSGestureEvent" in window; - } - function _isEdgeChromium(_b) { - const b = _b || kurentoUtils.WebRtcPeer.browser; - return b.name === 'Edge' && !("MSGestureEvent" in window); - } - function _setPos(v, pos) { - if (v.dialog('instance')) { - v.dialog('widget').css(pos); - } - } - function _askPermission(callback) { - const perm = $('#ask-permission'); - if (undefined === perm.dialog('instance')) { - perm.data('callbacks', []).dialog({ - appendTo: '.room-block .room-container' - , dialogClass: "ask-video-play-permission" - , autoOpen: true - , buttons: [ - { - text: perm.data('btn-ok') - , click: function() { - while (perm.data('callbacks').length > 0) { - perm.data('callbacks').pop()(); - } - $(this).dialog('close'); - } - } - ] - }); - } else if (!perm.dialog('isOpen')) { - perm.dialog('open') - } - perm.data('callbacks').push(callback); - } - function _disconnect(node) { - try { - node.disconnect(); //this one can throw - } catch (e) { - //no-op - } - } - function _sharingSupported() { - const b = kurentoUtils.WebRtcPeer.browser; - return (b.name === 'Edge' && b.major > 16) - || (b.name === 'Firefox') - || (b.name === 'Opera') - || (b.name === 'Yandex') - || _isChrome(b) - || _isEdgeChromium(b) - || (b.name === 'Mozilla' && b.major > 4); - } - function _highlight(el, clazz, count) { - if (!el || el.length < 1 || el.hasClass('disabled') || count < 0) { - return; - } - el.addClass(clazz).delay(2000).queue(function(next) { - el.removeClass(clazz).delay(2000).queue(function(next1) { - _highlight(el, clazz, --count); - next1(); - }); - next(); - }); - } - - self.getVid = _getVid; - self.isSharing = _isSharing; - self.isRecording = _isRecording; - self.hasMic = _hasMic; - self.hasCam = _hasCam; - self.hasVideo = _hasVideo; - self.getRects = _getRects; - self.getPos = _getPos; - self.container = _container; - self.arrange = _arrange; - self.arrangeResize = _arrangeResize; - self.cleanStream = _cleanStream; - self.cleanPeer = _cleanPeer; - self.addIceServers = function(opts, m) { - if (m && m.iceServers && m.iceServers.length > 0) { - opts.configuration = {iceServers: m.iceServers}; - } - return opts; - }; - self.isEdge = _isEdge; - self.isEdgeChromium = _isEdgeChromium; - self.isChrome = _isChrome; - self.setPos = _setPos; - self.askPermission = _askPermission; - self.disconnect = _disconnect; - self.sharingSupported = _sharingSupported; - self.highlight = _highlight; - return self; -})(); -var Volume = (function() { - let video, vol, drop, slider, handleEl, hideTimer = null - , lastVolume = 50, muted = false; - - function __cancelHide() { - if (hideTimer) { - clearTimeout(hideTimer); - hideTimer = null; - } - } - function __hideDrop() { - __cancelHide(); - hideTimer = setTimeout(() => { - drop.hide(); - hideTimer = null; - }, 3000); - } - - function _create(_video) { - video = _video; - _destroy(); - const uid = video.stream().uid - , cuid = video.stream().cuid - , volId = 'volume-' + uid; - vol = OmUtil.tmpl('#volume-control-stub', volId) - slider = vol.find('.slider'); - drop = vol.find('.dropdown-menu'); - vol.on('mouseenter', function(e) { - e.stopImmediatePropagation(); - drop.show(); - __hideDrop() - }) - .click(function(e) { - e.stopImmediatePropagation(); - OmUtil.roomAction({action: 'mute', uid: cuid, mute: !muted}); - _mute(!muted); - drop.hide(); - return false; - }).dblclick(function(e) { - e.stopImmediatePropagation(); - return false; - }); - drop.on('mouseenter', function() { - __cancelHide(); - }); - drop.on('mouseleave', function() { - __hideDrop(); - }); - handleEl = vol.find('.handle'); - slider.slider({ - orientation: 'vertical' - , range: 'min' - , min: 0 - , max: 100 - , value: lastVolume - , create: function() { - handleEl.text($(this).slider('value')); - } - , slide: function(event, ui) { - _handle(ui.value); - } - }); - _handle(lastVolume); - _mute(muted); - return vol; - } - function _handle(val) { - handleEl.text(val); - const vidEl = video.video() - , data = vidEl.data(); - if (video.stream().self) { - if (data.gainNode) { - data.gainNode.gain.value = val / 100; - } - } else { - vidEl[0].volume = val / 100; - } - const ico = vol.find('a'); - if (val > 0 && ico.hasClass('volume-off')) { - ico.toggleClass('volume-off volume-on'); - video.handleMicStatus(true); - } else if (val === 0 && ico.hasClass('volume-on')) { - ico.toggleClass('volume-on volume-off'); - video.handleMicStatus(false); - } - } - function _mute(mute) { - if (!slider) { - return; - } - muted = mute; - if (mute) { - const val = slider.slider('option', 'value'); - if (val > 0) { - lastVolume = val; - } - slider.slider('option', 'value', 0); - _handle(0); - } else { - slider.slider('option', 'value', lastVolume); - _handle(lastVolume); - } - } - function _destroy() { - if (vol) { - vol.remove(); - vol = null; - } - } - - return { - create: _create - , handle: _handle - , mute: _mute - , muted: function() { - return muted; - } - , destroy: _destroy - }; -}); diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video.js index b556209..88abee7 100644 --- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video.js +++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video.js @@ -170,7 +170,7 @@ var Video = (function() { return OmUtil.error(error); } if (data.analyser) { - level = MicLevel(); + level = new MicLevel(); level.meter(data.analyser, lm, _micActivity, OmUtil.error); } data.rtcPeer.generateOffer(function(genErr, offerSdp) { diff --git a/pom.xml b/pom.xml index 3ec8417..b6ea9be 100644 --- a/pom.xml +++ b/pom.xml @@ -122,7 +122,7 @@ <jain-sip.version>1.2.307</jain-sip.version><!-- other versions are broken! --> <jasny-bootstrap.version>3.1.3-2</jasny-bootstrap.version> <!-- Exclude all generated code --> - <sonar.exclusions>file:**/generated-sources/**, file:**/jquery-ui.css, file:**/fabric.js, file:**/cssemoticons.js, file:**/adapter-latest.js, file:**/kurento-utils.js, file:**/NoSleep.js, file:**/MathJax.js</sonar.exclusions> + <sonar.exclusions>file:**/generated-sources/**, file:**/jquery-ui.css, file:**/fabric.js, file:**/cssemoticons.js, file:**/NoSleep.js, file:**/MathJax.js</sonar.exclusions> <sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin> <sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis> <sonar.junit.reportPaths>target/surefire-reports</sonar.junit.reportPaths>