Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
207d84d
Loaded "nouislider" (functions.php)
IgorA100 Jul 29, 2025
dde081a
Code of the new audio stream control slider (watch.php)
IgorA100 Jul 29, 2025
b3d9c3c
New audio stream control slider style (skin.css)
IgorA100 Jul 29, 2025
6398d35
Removed unused code (watch.js)
IgorA100 Jul 29, 2025
48c0e4b
noUiSlider
IgorA100 Jul 29, 2025
602d658
Code for controlling the new audio stream control slider (MonitorStre…
IgorA100 Jul 29, 2025
1be91c2
Event listener "click" for buttons with "id=^controlMute" move to ski…
IgorA100 Jul 29, 2025
fb4c1fb
Code optimization
IgorA100 Jul 29, 2025
68e98fc
Merge branch 'ZoneMinder:master' into patch-896443
IgorA100 Jul 29, 2025
a59a12d
For noUiSlider import only the "dist" folder and the "README.md" file
IgorA100 Jul 30, 2025
64f65f5
- Use folder "noUiSlider-15.8.1" instead of "noUiSlider"
IgorA100 Jul 30, 2025
8a7f703
Merge branch 'ZoneMinder:master' into patch-896443
IgorA100 Jul 30, 2025
0685336
Fix: Eslint
IgorA100 Jul 30, 2025
430a591
Fix: eslint
IgorA100 Jul 30, 2025
8a3220d
Added ESLint ignore "web/js/noUiSlider-15.8.1/"
IgorA100 Jul 30, 2025
6fe174a
Merge branch 'master' into patch-896443
IgorA100 Aug 1, 2025
c6efacd
Update MonitorStream.js
IgorA100 Aug 2, 2025
8779663
Merge branch 'master' into patch-896443
IgorA100 Aug 4, 2025
5f6db1f
Merge branch 'ZoneMinder:master' into patch-896443
IgorA100 Aug 5, 2025
47f5933
When quickly switching between cameras on the Watch page, the audioSt…
IgorA100 Aug 7, 2025
6804761
When quickly switching between cameras on the Watch page, the audioSt…
IgorA100 Aug 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ web/skins/classic/assets
web/js/janus.js
web/js/ajaxQueue.js
web/js/hls-1.5.20/
web/js/noUiSlider-15.8.1/
web/js/dms.js

# Cannot be parsed as JS
Expand Down
187 changes: 138 additions & 49 deletions web/js/MonitorStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,17 +360,18 @@ function MonitorStream(monitorData) {
old_stream.remove();
stream_container.appendChild(stream);
this.webrtc = stream; // track separately do to api differences between video tag and video-stream
this.set_stream_volume(this.muted ? 0.0 : this.volume/100);
if (-1 != this.player.indexOf('_')) {
stream.mode = this.player.substring(this.player.indexOf('_')+1);
}
document.querySelector('video').addEventListener('play', (e) => {
this.createVolumeSlider();
}, this);

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
this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout);
this.started = true;
this.streamListenerBind();

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

if (this.janusEnabled && ((!this.player) || (-1 !== this.player.indexOf('janus')))) {
let server;
document.querySelector('video').addEventListener('play', (e) => {
this.createVolumeSlider();
}, this);
if (ZM_JANUS_PATH) {
server = ZM_JANUS_PATH;
} else if (this.server_id && Servers[this.server_id]) {
Expand Down Expand Up @@ -426,6 +430,9 @@ function MonitorStream(monitorData) {
const useSSL = (url.protocol == 'https');

const rtsp2webModUrl = url;
document.querySelector('video').addEventListener('play', (e) => {
this.createVolumeSlider();
}, this);
rtsp2webModUrl.username = '';
rtsp2webModUrl.password = '';
//.urlParts.length > 1 ? urlParts[1] : urlParts[0]; // drop the username and password for viewing
Expand Down Expand Up @@ -465,7 +472,6 @@ function MonitorStream(monitorData) {
this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout);
this.started = true;
this.streamListenerBind();
$j('#volumeControls').show();
this.updateStreamInfo('RTSP2Web ' + this.RTSP2WebType);
return;
} else {
Expand Down Expand Up @@ -751,63 +757,146 @@ function MonitorStream(monitorData) {
this.onplay = func;
};

this.volume_slider = null;
this.volume = 0.0; // Half

this.setup_volume = function(slider) {
this.volume_slider = slider;
this.volume_slider.addEventListener('click', (e) => {
const x = e.pageX - this.volume_slider.getBoundingClientRect().left; // or e.offsetX (less support, though)
const clickedValue = parseInt(x * this.volume_slider.max / this.volume_slider.offsetWidth);
this.volume_slider.value = clickedValue;
this.set_volume(clickedValue);
this.muted = clickedValue ? false : true;
setCookie('zmWatchMuted', this.muted);
this.mute_btn.firstElementChild.innerHTML = (this.muted ? 'volume_off' : 'volume_up');
});
this.volume = this.volume_slider.value;
this.getVolumeSlider = function(mid) {
// On Watch page slider has no ID, on Montage page it has ID
return (document.getElementById('volumeSlider')) ? document.getElementById('volumeSlider') : document.getElementById('volumeSlider'+mid);
};

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

/* Takes volume as percentage */
this.set_stream_volume = function(volume) {
if (this.webrtc && this.webrtc.volume ) {
this.webrtc.volume(volume);
} else {
const stream = this.getElement();
stream.volume = volume;
this.getAudioStraem = function(mid) {
/*
Go2RTC uses <video-stream id='liveStreamXX'><video></video></video-stream>,
RTSP2Web uses <video id='liveStreamXX'></video>
This.getElement() may need to be changed, but the implications of such a change need to be analyzed
*/
return (document.querySelector('#liveStream'+mid + ' video') || document.getElementById('liveStream'+mid));
};

this.listenerVolumechange = function(el) {
// System audio level change
const mid = this.id;
const audioStream = el.target;
const volumeSlider = this.getVolumeSlider(mid);
const iconMute = this.getIconMute(mid);
if (volumeSlider.allowSetValue) {
if (audioStream.muted === true) {
iconMute.innerHTML = 'volume_off';
volumeSlider.classList.add('noUi-mute');
} else {
iconMute.innerHTML = 'volume_up';
volumeSlider.classList.remove('noUi-mute');
}
volumeSlider.noUiSlider.set(audioStream.volume * 100);
}
setCookie('zmWatchMuted', audioStream.muted);
setCookie('zmWatchVolume', parseInt(audioStream.volume * 100));
volumeSlider.setAttribute('data-muted', audioStream.muted);
volumeSlider.setAttribute('data-volume', parseInt(audioStream.volume * 100));
};

this.mute_btn = null;
this.muted = false;
this.createVolumeSlider = function() {
const mid = this.id;
const volumeSlider = this.getVolumeSlider(mid);
const iconMute = this.getIconMute(mid);
const audioStream = this.getAudioStraem(mid);
if (!volumeSlider) return;
const defaultVolume = (volumeSlider.getAttribute("data-volume") || 50);
if (volumeSlider.noUiSlider) volumeSlider.noUiSlider.destroy();

$j('#volumeControls').show();
noUiSlider.create(volumeSlider, {
start: [(defaultVolume) ? defaultVolume : audioStream.volume * 100],
step: 1,
//behaviour: 'unconstrained',
behaviour: 'tap',
connect: [true, false],
range: {
'min': 0,
'max': 100
},
/*tooltips: [
//true,
{ to: function(value) { return value.toFixed(0) + '%'; } }
],*/
});
volumeSlider.allowSetValue = true;
volumeSlider.noUiSlider.on('update', function onUpdateUiSlider(values, handle) {
if (audioStream) {
audioStream.volume = values[0]/100;
if (values[0] > 0 && !audioStream.muted) {
iconMute.innerHTML = 'volume_up';
volumeSlider.classList.remove('noUi-mute');
} else {
iconMute.innerHTML = 'volume_off';
volumeSlider.classList.add('noUi-mute');
}
}
//console.log("Audio volume slider event: 'update'");
});
volumeSlider.noUiSlider.on('end', function onEndUiSlider(values, handle) {
volumeSlider.allowSetValue = true;
//console.log("Audio volume slider event: 'end'");
});
volumeSlider.noUiSlider.on('start', function onStartUiSlider(values, handle) {
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...
//console.log("Audio volume slider event: 'start'");
});
volumeSlider.noUiSlider.on('set', function onSetUiSlider(values, handle) {
//console.log("Audio volume slider event: 'set'");
});
volumeSlider.noUiSlider.on('slide', function onSlideUiSlider(values, handle) {
if (audioStream.volume > 0 && audioStream.muted) {
iconMute.innerHTML = 'volume_up';
audioStream.muted = false;
}
//console.log("Audio volume slider event: 'slide'");
});

this.setup_mute = function(mute_btn) {
this.mute_btn = mute_btn;
this.mute_btn.onclick = () => {
this.muted = !this.muted;
setCookie('zmWatchMuted', this.muted);
this.mute_btn.firstElementChild.innerHTML = (this.muted ? 'volume_off' : 'volume_up');
if (volumeSlider.getAttribute("data-muted") !== "true") {
this.controlMute('off');
} else {
this.controlMute('on');
}

if (this.muted === false) {
this.set_stream_volume(this.volume/100); // lastvolume
if (this.volume_slider) this.volume_slider.value = this.volume;
if (audioStream) {
audioStream.addEventListener('volumechange', (event) => {
this.listenerVolumechange(event);
});
}
};

/*
* mode: switch, on, off
*/
this.controlMute = function(mode = 'switch') {
const mid = this.id;
const volumeSlider = this.getVolumeSlider(mid);
const audioStream = this.getAudioStraem(mid);
const iconMute = this.getIconMute(mid);
if (!audioStream || !iconMute) return;
if (mode=='switch') {
if (audioStream.muted) {
audioStream.muted = false;
iconMute.innerHTML = 'volume_up';
volumeSlider.classList.add('noUi-mute');
audioStream.volume = volumeSlider.noUiSlider.get() / 100;
} else {
this.set_stream_volume(0.0);
if (this.volume_slider) this.volume_slider.value = 0;
audioStream.muted = true;
iconMute.innerHTML = 'volume_off';
volumeSlider.classList.remove('noUi-mute');
}
};
this.muted = (this.mute_btn.firstElementChild.innerHTML == 'volume_off');
if (this.muted) {
// muted, adjust volume bar
this.set_stream_volume(0.0);
if (this.volume_slider) this.volume_slider.value = 0;
} else if (mode=='on') {
audioStream.muted = true;
iconMute.innerHTML = 'volume_off';
volumeSlider.classList.add('noUi-mute');
} else if (mode=='off') {
audioStream.muted = false;
iconMute.innerHTML = 'volume_up';
volumeSlider.classList.remove('noUi-mute');
}
};

Expand Down
42 changes: 42 additions & 0 deletions web/js/noUiSlider-15.8.1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# noUiSlider

noUiSlider is a lightweight JavaScript range slider.

- **Accessible** with `aria` and keyboard support
- GPU animated: no reflows, so fast; even on older devices
- All modern browsers and IE > 9 are supported
- **No dependencies**
- Fully **responsive**
- **Multi-touch support** on Android, iOS and Windows devices
- Tons of [examples](https://refreshless.com/nouislider/examples) and answered [Stack Overflow questions](https://stackoverflow.com/questions/tagged/nouislider)

License
-------
noUiSlider is licensed [MIT](https://choosealicense.com/licenses/mit/).

It can be used **for free** and **without any attribution**, in any personal or commercial project.

[Documentation](https://refreshless.com/nouislider/)
-------
An extensive documentation, including **examples**, **options** and **configuration details**, is available here:

[noUiSlider documentation](https://refreshless.com/nouislider/).

Contributing
------------

See [Contributing](CONTRIBUTING.md).

Sponsorship
-----------

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.

Please consider sponsoring the project by clicking the "❤ Sponsor" button above. Thanks!

Tooling
-------

Cross-browser testing kindly provided by BrowserStack.

[![Tested with BrowserStack](documentation/assets/browserstack-logo-380x90.png)](http://browserstack.com/)
Loading