Skip to content

Commit f047176

Browse files
Merge pull request #4394 from IgorA100/patch-896443
Feat: New design of the audio stream control slider
2 parents bf41af8 + 6804761 commit f047176

File tree

15 files changed

+5437
-58
lines changed

15 files changed

+5437
-58
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ web/skins/classic/assets
2121
web/js/janus.js
2222
web/js/ajaxQueue.js
2323
web/js/hls-1.5.20/
24+
web/js/noUiSlider-15.8.1/
2425
web/js/dms.js
2526

2627
# Cannot be parsed as JS

web/js/MonitorStream.js

Lines changed: 138 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -360,17 +360,18 @@ function MonitorStream(monitorData) {
360360
old_stream.remove();
361361
stream_container.appendChild(stream);
362362
this.webrtc = stream; // track separately do to api differences between video tag and video-stream
363-
this.set_stream_volume(this.muted ? 0.0 : this.volume/100);
364363
if (-1 != this.player.indexOf('_')) {
365364
stream.mode = this.player.substring(this.player.indexOf('_')+1);
366365
}
366+
document.querySelector('video').addEventListener('play', (e) => {
367+
this.createVolumeSlider();
368+
}, this);
367369

368370
clearInterval(this.statusCmdTimer); // Fix for issues in Chromium when quickly hiding/showing a page. Doesn't clear statusCmdTimer when minimizing a page https://stackoverflow.com/questions/9501813/clearinterval-not-working
369371
this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout);
370372
this.started = true;
371373
this.streamListenerBind();
372374

373-
$j('#volumeControls').show();
374375
if (typeof observerMontage !== 'undefined') observerMontage.observe(stream);
375376
this.activePlayer = 'go2rtc';
376377
return;
@@ -381,6 +382,9 @@ function MonitorStream(monitorData) {
381382

382383
if (this.janusEnabled && ((!this.player) || (-1 !== this.player.indexOf('janus')))) {
383384
let server;
385+
document.querySelector('video').addEventListener('play', (e) => {
386+
this.createVolumeSlider();
387+
}, this);
384388
if (ZM_JANUS_PATH) {
385389
server = ZM_JANUS_PATH;
386390
} else if (this.server_id && Servers[this.server_id]) {
@@ -426,6 +430,9 @@ function MonitorStream(monitorData) {
426430
const useSSL = (url.protocol == 'https');
427431

428432
const rtsp2webModUrl = url;
433+
document.querySelector('video').addEventListener('play', (e) => {
434+
this.createVolumeSlider();
435+
}, this);
429436
rtsp2webModUrl.username = '';
430437
rtsp2webModUrl.password = '';
431438
//.urlParts.length > 1 ? urlParts[1] : urlParts[0]; // drop the username and password for viewing
@@ -465,7 +472,6 @@ function MonitorStream(monitorData) {
465472
this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout);
466473
this.started = true;
467474
this.streamListenerBind();
468-
$j('#volumeControls').show();
469475
this.updateStreamInfo('RTSP2Web ' + this.RTSP2WebType);
470476
return;
471477
} else {
@@ -762,63 +768,146 @@ function MonitorStream(monitorData) {
762768
this.onplay = func;
763769
};
764770

765-
this.volume_slider = null;
766-
this.volume = 0.0; // Half
767-
768-
this.setup_volume = function(slider) {
769-
this.volume_slider = slider;
770-
this.volume_slider.addEventListener('click', (e) => {
771-
const x = e.pageX - this.volume_slider.getBoundingClientRect().left; // or e.offsetX (less support, though)
772-
const clickedValue = parseInt(x * this.volume_slider.max / this.volume_slider.offsetWidth);
773-
this.volume_slider.value = clickedValue;
774-
this.set_volume(clickedValue);
775-
this.muted = clickedValue ? false : true;
776-
setCookie('zmWatchMuted', this.muted);
777-
this.mute_btn.firstElementChild.innerHTML = (this.muted ? 'volume_off' : 'volume_up');
778-
});
779-
this.volume = this.volume_slider.value;
771+
this.getVolumeSlider = function(mid) {
772+
// On Watch page slider has no ID, on Montage page it has ID
773+
return (document.getElementById('volumeSlider')) ? document.getElementById('volumeSlider') : document.getElementById('volumeSlider'+mid);
780774
};
781775

782-
/* Takes volume as 0->100 */
783-
this.set_volume = function(volume) {
784-
this.volume = volume;
785-
this.set_stream_volume(volume/100);
786-
setCookie('zmWatchVolume', this.volume);
776+
this.getIconMute = function(mid) {
777+
// On Watch page icon has no ID, on Montage page it has ID
778+
return (document.getElementById('controlMute')) ? document.getElementById('controlMute') : document.getElementById('controlMute'+mid);
787779
};
788780

789-
/* Takes volume as percentage */
790-
this.set_stream_volume = function(volume) {
791-
if (this.webrtc && this.webrtc.volume ) {
792-
this.webrtc.volume(volume);
793-
} else {
794-
const stream = this.getElement();
795-
stream.volume = volume;
781+
this.getAudioStraem = function(mid) {
782+
/*
783+
Go2RTC uses <video-stream id='liveStreamXX'><video></video></video-stream>,
784+
RTSP2Web uses <video id='liveStreamXX'></video>
785+
This.getElement() may need to be changed, but the implications of such a change need to be analyzed
786+
*/
787+
return (document.querySelector('#liveStream'+mid + ' video') || document.getElementById('liveStream'+mid));
788+
};
789+
790+
this.listenerVolumechange = function(el) {
791+
// System audio level change
792+
const mid = this.id;
793+
const audioStream = el.target;
794+
const volumeSlider = this.getVolumeSlider(mid);
795+
const iconMute = this.getIconMute(mid);
796+
if (volumeSlider.allowSetValue) {
797+
if (audioStream.muted === true) {
798+
iconMute.innerHTML = 'volume_off';
799+
volumeSlider.classList.add('noUi-mute');
800+
} else {
801+
iconMute.innerHTML = 'volume_up';
802+
volumeSlider.classList.remove('noUi-mute');
803+
}
804+
volumeSlider.noUiSlider.set(audioStream.volume * 100);
796805
}
806+
setCookie('zmWatchMuted', audioStream.muted);
807+
setCookie('zmWatchVolume', parseInt(audioStream.volume * 100));
808+
volumeSlider.setAttribute('data-muted', audioStream.muted);
809+
volumeSlider.setAttribute('data-volume', parseInt(audioStream.volume * 100));
797810
};
798811

799-
this.mute_btn = null;
800-
this.muted = false;
812+
this.createVolumeSlider = function() {
813+
const mid = this.id;
814+
const volumeSlider = this.getVolumeSlider(mid);
815+
const iconMute = this.getIconMute(mid);
816+
const audioStream = this.getAudioStraem(mid);
817+
if (!volumeSlider) return;
818+
const defaultVolume = (volumeSlider.getAttribute("data-volume") || 50);
819+
if (volumeSlider.noUiSlider) volumeSlider.noUiSlider.destroy();
820+
821+
$j('#volumeControls').show();
822+
noUiSlider.create(volumeSlider, {
823+
start: [(defaultVolume) ? defaultVolume : audioStream.volume * 100],
824+
step: 1,
825+
//behaviour: 'unconstrained',
826+
behaviour: 'tap',
827+
connect: [true, false],
828+
range: {
829+
'min': 0,
830+
'max': 100
831+
},
832+
/*tooltips: [
833+
//true,
834+
{ to: function(value) { return value.toFixed(0) + '%'; } }
835+
],*/
836+
});
837+
volumeSlider.allowSetValue = true;
838+
volumeSlider.noUiSlider.on('update', function onUpdateUiSlider(values, handle) {
839+
if (audioStream) {
840+
audioStream.volume = values[0]/100;
841+
if (values[0] > 0 && !audioStream.muted) {
842+
iconMute.innerHTML = 'volume_up';
843+
volumeSlider.classList.remove('noUi-mute');
844+
} else {
845+
iconMute.innerHTML = 'volume_off';
846+
volumeSlider.classList.add('noUi-mute');
847+
}
848+
}
849+
//console.log("Audio volume slider event: 'update'");
850+
});
851+
volumeSlider.noUiSlider.on('end', function onEndUiSlider(values, handle) {
852+
volumeSlider.allowSetValue = true;
853+
//console.log("Audio volume slider event: 'end'");
854+
});
855+
volumeSlider.noUiSlider.on('start', function onStartUiSlider(values, handle) {
856+
volumeSlider.allowSetValue = false; // Let's prohibit changing the Value using the "Set" method, otherwise there will be lags and collapse when directly moving the slider with the mouse...
857+
//console.log("Audio volume slider event: 'start'");
858+
});
859+
volumeSlider.noUiSlider.on('set', function onSetUiSlider(values, handle) {
860+
//console.log("Audio volume slider event: 'set'");
861+
});
862+
volumeSlider.noUiSlider.on('slide', function onSlideUiSlider(values, handle) {
863+
if (audioStream.volume > 0 && audioStream.muted) {
864+
iconMute.innerHTML = 'volume_up';
865+
audioStream.muted = false;
866+
}
867+
//console.log("Audio volume slider event: 'slide'");
868+
});
801869

802-
this.setup_mute = function(mute_btn) {
803-
this.mute_btn = mute_btn;
804-
this.mute_btn.onclick = () => {
805-
this.muted = !this.muted;
806-
setCookie('zmWatchMuted', this.muted);
807-
this.mute_btn.firstElementChild.innerHTML = (this.muted ? 'volume_off' : 'volume_up');
870+
if (volumeSlider.getAttribute("data-muted") !== "true") {
871+
this.controlMute('off');
872+
} else {
873+
this.controlMute('on');
874+
}
808875

809-
if (this.muted === false) {
810-
this.set_stream_volume(this.volume/100); // lastvolume
811-
if (this.volume_slider) this.volume_slider.value = this.volume;
876+
if (audioStream) {
877+
audioStream.addEventListener('volumechange', (event) => {
878+
this.listenerVolumechange(event);
879+
});
880+
}
881+
};
882+
883+
/*
884+
* mode: switch, on, off
885+
*/
886+
this.controlMute = function(mode = 'switch') {
887+
const mid = this.id;
888+
const volumeSlider = this.getVolumeSlider(mid);
889+
const audioStream = this.getAudioStraem(mid);
890+
const iconMute = this.getIconMute(mid);
891+
if (!audioStream || !iconMute) return;
892+
if (mode=='switch') {
893+
if (audioStream.muted) {
894+
audioStream.muted = false;
895+
iconMute.innerHTML = 'volume_up';
896+
volumeSlider.classList.add('noUi-mute');
897+
audioStream.volume = volumeSlider.noUiSlider.get() / 100;
812898
} else {
813-
this.set_stream_volume(0.0);
814-
if (this.volume_slider) this.volume_slider.value = 0;
899+
audioStream.muted = true;
900+
iconMute.innerHTML = 'volume_off';
901+
volumeSlider.classList.remove('noUi-mute');
815902
}
816-
};
817-
this.muted = (this.mute_btn.firstElementChild.innerHTML == 'volume_off');
818-
if (this.muted) {
819-
// muted, adjust volume bar
820-
this.set_stream_volume(0.0);
821-
if (this.volume_slider) this.volume_slider.value = 0;
903+
} else if (mode=='on') {
904+
audioStream.muted = true;
905+
iconMute.innerHTML = 'volume_off';
906+
volumeSlider.classList.add('noUi-mute');
907+
} else if (mode=='off') {
908+
audioStream.muted = false;
909+
iconMute.innerHTML = 'volume_up';
910+
volumeSlider.classList.remove('noUi-mute');
822911
}
823912
};
824913

web/js/noUiSlider-15.8.1/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# noUiSlider
2+
3+
noUiSlider is a lightweight JavaScript range slider.
4+
5+
- **Accessible** with `aria` and keyboard support
6+
- GPU animated: no reflows, so fast; even on older devices
7+
- All modern browsers and IE > 9 are supported
8+
- **No dependencies**
9+
- Fully **responsive**
10+
- **Multi-touch support** on Android, iOS and Windows devices
11+
- Tons of [examples](https://refreshless.com/nouislider/examples) and answered [Stack Overflow questions](https://stackoverflow.com/questions/tagged/nouislider)
12+
13+
License
14+
-------
15+
noUiSlider is licensed [MIT](https://choosealicense.com/licenses/mit/).
16+
17+
It can be used **for free** and **without any attribution**, in any personal or commercial project.
18+
19+
[Documentation](https://refreshless.com/nouislider/)
20+
-------
21+
An extensive documentation, including **examples**, **options** and **configuration details**, is available here:
22+
23+
[noUiSlider documentation](https://refreshless.com/nouislider/).
24+
25+
Contributing
26+
------------
27+
28+
See [Contributing](CONTRIBUTING.md).
29+
30+
Sponsorship
31+
-----------
32+
33+
noUiSlider is a stable project that still receives a lot of feature requests. A lot of these are interesting, but require a good amount of effort to implement, test and document. Sponsorship of this project will allow me to spend some more of my time on these feature requests.
34+
35+
Please consider sponsoring the project by clicking the "❤ Sponsor" button above. Thanks!
36+
37+
Tooling
38+
-------
39+
40+
Cross-browser testing kindly provided by BrowserStack.
41+
42+
[![Tested with BrowserStack](documentation/assets/browserstack-logo-380x90.png)](http://browserstack.com/)

0 commit comments

Comments
 (0)