Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
bd56f17
Loading "nouislider" and "audioMotion-analyzer" (functions.php)
IgorA100 Jul 26, 2025
2e8ad4f
Added <audio-motion> (Monitor.php)
IgorA100 Jul 26, 2025
3a25c84
Merge branch 'master' into patch-435415
IgorA100 Aug 9, 2025
826c1e3
Update functions.php
IgorA100 Aug 9, 2025
c405832
Merge branch 'master' into patch-435415
IgorA100 Mar 14, 2026
524ab91
Added audio visualization
IgorA100 Mar 14, 2026
d130f49
Audiovisualization
IgorA100 Mar 15, 2026
e677f6a
Remove extra space
IgorA100 Mar 15, 2026
a958d83
Audiovisualization
IgorA100 Mar 15, 2026
10fb918
Remove extra space
IgorA100 Mar 15, 2026
c49b48e
Audiovisualization
IgorA100 Mar 16, 2026
1cf92b4
Style adjustments
IgorA100 Mar 16, 2026
82095fe
If we don't display the video, then display the information block on …
IgorA100 Mar 16, 2026
d9361ea
Fix parent
IgorA100 Mar 16, 2026
33b9b45
Add "#whatDisplayControl" (event.php)
IgorA100 Mar 16, 2026
79f7db1
Add $whatDisplay (event.php)
IgorA100 Mar 16, 2026
7992fa4
Add changeWhatDisplay() (event.js)
IgorA100 Mar 16, 2026
1af54ad
Creating a volume slider on the Montage page to simultaneously contro…
IgorA100 Mar 18, 2026
c4dcf84
Fix: Eslint
IgorA100 Mar 18, 2026
9122a15
Removed an unused constant
IgorA100 Mar 18, 2026
1a1c219
skin.js
IgorA100 Mar 19, 2026
89a9839
Merge branch 'ZoneMinder:master' into patch-435415
IgorA100 Mar 19, 2026
b4dc754
Fix: Eslint (MonitorStream.js)
IgorA100 Mar 19, 2026
291ebb8
Update skin.js
IgorA100 Mar 19, 2026
4786bb5
Feat: Run changeWhatDisplay() without refreshing the page (watch.js)
IgorA100 Mar 20, 2026
3765301
Set maxFPS=30 instead of 50 (audioMotionAnalyzer.js)
IgorA100 Mar 20, 2026
495e6e8
Update web/skins/classic/js/skin.js
IgorA100 Mar 21, 2026
b468fa7
Update web/skins/classic/views/event.php
IgorA100 Mar 21, 2026
f3a2910
Instead of audioMotion.play(), use audioMotion.init() (MonitorStream.js)
IgorA100 Mar 21, 2026
3a37569
Update web/skins/classic/views/watch.php
IgorA100 Mar 21, 2026
686fd09
Update web/skins/classic/views/montage.php
IgorA100 Mar 21, 2026
7b96060
Update web/skins/classic/views/event.php
IgorA100 Mar 21, 2026
bf2fe61
Declare constants (skin.js)
IgorA100 Mar 21, 2026
ce0fc7a
Update web/skins/classic/js/skin.js
IgorA100 Mar 21, 2026
6f8a124
We'll use $cspNonce (functions.php)
IgorA100 Mar 21, 2026
364de38
Load audioMotionAnalyzer.js only on Watch, Montage, and Event pages (…
IgorA100 Mar 21, 2026
025d9fc
Added translation for "OPTIONS_WHATTODISPLAY" en_gb.php
IgorA100 Mar 21, 2026
5c5c05d
Removed extra semicolons (watch.js)
IgorA100 Mar 21, 2026
c4bab6a
Merge branch 'ZoneMinder:master' into patch-435415
IgorA100 Mar 30, 2026
e694130
Merge branch 'ZoneMinder:master' into patch-435415
IgorA100 Apr 1, 2026
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
29 changes: 29 additions & 0 deletions web/includes/Monitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ public static function getStreamChannelOptions() {
'AnalysisImage' => 'FullColour',
'Enabled' => array('type'=>'boolean','default'=>1),
'Decoding' => 'Always',
'WhatDisplay' => 'OnlyVideo',
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a new WhatDisplay monitor field in defaults and UI, but the PR doesn’t include a DB migration/update SQL in db/ to add the Monitors.WhatDisplay column. Without an update script, installs/upgrades will hit SQL/ORM errors when accessing $monitor->WhatDisplay(). Add a db/zm_update-*.sql entry (and/or schema update) as part of this change.

Suggested change
'WhatDisplay' => 'OnlyVideo',

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to break anything, so I didn't add the SQL migration to the code.
You need to add the following code:
ALTER TABLE MonitorsADDWhatDisplayenum('OnlyVideo','OnlyAudioVisualization','VideoAudioVisualization') NOT NULL DEFAULT 'OnlyVideo' AFTERDecoding;

'RTSP2WebEnabled' => array('type'=>'integer','default'=>0),
'DefaultPlayer' => '',
'StreamChannel' => 'Restream',
Expand Down Expand Up @@ -1078,6 +1079,7 @@ function getMonitorStateHTML() {
$html = '
<div id="monitorStatus'.$this->Id().'" class="monitorStatus">
<div class="stream-info">
<div class="stream-info-status-track"></div>
<div class="stream-info-status"></div>
<div class="stream-info-mode"></div>
</div>
Expand Down Expand Up @@ -1109,6 +1111,16 @@ function getMonitorStateHTML() {
*/
function getStreamHTML($options) {
global $basename;
global $view;

$whatDisplay = (isset($_COOKIE["zmWhatDisplay"])) ? strtolower($_COOKIE["zmWhatDisplay"]) : 'default';
$dataNotDisplayVideo = 'false';

if (false !== strpos($whatDisplay, 'default')) { // Default monitor settings
if (false === (strpos(strtolower($this->WhatDisplay()), 'video'))) $dataNotDisplayVideo = 'true';
} else {
if (false === (strpos($whatDisplay, 'video'))) $dataNotDisplayVideo = 'true';
}

if (isset($options['scale']) and $options['scale'] != '' and $options['scale'] != 'fixed') {
if ($options['scale'] != 'auto' && $options['scale'] != '0') {
Expand Down Expand Up @@ -1175,6 +1187,7 @@ function getStreamHTML($options) {
class="monitorStream imageFeed"
data-monitor-id="'. $this->Id() .'"
data-width="'. $this->ViewWidth() .'"
data-not-display-video="'. $dataNotDisplayVideo .'"
data-height="'.$this->ViewHeight() .'" style="'.
#(($options['width'] and ($options['width'] != '0px')) ? 'width: '.$options['width'].';' : '').
#(($options['height'] and ($options['height'] != '0px')) ? 'height: '.$options['height'].';' : '').
Expand Down Expand Up @@ -1270,6 +1283,22 @@ class="monitorStream imageFeed"
//if ((!ZM_WEB_COMPACT_MONTAGE) && ($this->Type() != 'WebSite')) {
$html .= $this->getMonitorStateHTML();
}
$html .= '
<audio-motion id="audioVisualization'.$this->Id().'" class="audio-visualization">
'.PHP_EOL;
if ($view == 'montage') {
$html .= '
<div id="audioControlPanel'.$this->Id().'" class="audio-control-panel">
<div id="volumeControls'.$this->Id().'" class="disabled volume">
<div id="volumeSlider'.$this->Id().'" data-volume="50" data-muted="true" class="volumeSlider noUi-horizontal noUi-base noUi-round"></div>
<i id="controlMute'.$this->Id().'" class="audio-control-mute material-icons md-22"></i>
</div>
</div>
<canvas></canvas>
'.PHP_EOL;
}
$html .= '
</audio-motion>'.PHP_EOL;
$html .= PHP_EOL.'</div></div><!--.grid-stack-item-content--></div><!--.grid-stack-item-->'.PHP_EOL;
return $html;
} // end getStreamHTML
Expand Down
176 changes: 43 additions & 133 deletions web/js/MonitorStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function MonitorStream(monitorData) {
this.server_id = monitorData.server_id;
this.scale = monitorData.scale ? parseInt(monitorData.scale) : 100;
this.status = {capturefps: 0, analysisfps: 0}; // json object with alarmstatus, fps etc
this.whatDisplay = monitorData.whatDisplay;
this.lastAlarmState = STATE_IDLE;
this.statusCmdTimer = null; // timer for requests using ajax to get monitor status
this.statusCmdParms = {
Expand Down Expand Up @@ -430,7 +431,8 @@ function MonitorStream(monitorData) {

//console.log('start go2rtcenabled:', this.Go2RTCEnabled, 'this.player:', this.player, 'muted', this.muted);

$j('#volumeControls'+this.id).hide();
//$j('#volumeControls'+this.id).hide();
$j('#volumeControls'+this.id).addClass('disabled');
$j('#delay'+this.id).addClass('hidden');

if (this.Go2RTCEnabled && ((!this.player) || (-1 !== this.player.indexOf('go2rtc')))) {
Expand Down Expand Up @@ -640,6 +642,7 @@ function MonitorStream(monitorData) {
this.handlerEventListener['killStream'] = this.streamListenerBind();
this.activePlayer = 'zms';
this.updateStreamInfo('ZMS MJPEG');
hideAudioMotion(this.id);
}; // this.start

this.setSrcInfoBlock = function() {
Expand Down Expand Up @@ -728,7 +731,14 @@ function MonitorStream(monitorData) {
infoBlock.style.left = '50%';
infoBlock.style.transform = 'translate(-50%, -50%)';
infoBlock.style.pointerEvents = 'none';
this.getElement().parentNode.appendChild(infoBlock);
let node = null;
const _imageFeed = document.getElementById('imageFeed'+this.id);
if (_imageFeed && _imageFeed.getAttribute('data-not-display-video') === 'true') {
node = document.getElementById("audioVisualization" + this.id) || this.getElement();
} else {
node = this.getElement();
}
node.parentNode.appendChild(infoBlock);
currentInfoBlock = infoBlock;
}
return currentInfoBlock;
Expand Down Expand Up @@ -803,6 +813,7 @@ function MonitorStream(monitorData) {
} else {
console.log("Unknown activePlayer", this.activePlayer);
}
if (this.audioMotion && this.audioMotion.stop) this.audioMotion.stop();
this.activePlayer = '';
this.started = false;
};
Expand Down Expand Up @@ -941,6 +952,7 @@ function MonitorStream(monitorData) {
}
this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout);
}
if (this.audioMotion && this.audioMotion.init) this.audioMotion.init();
};

this.eventHandler = function(event) {
Expand Down Expand Up @@ -994,20 +1006,16 @@ function MonitorStream(monitorData) {
this.onplay = func;
};


this.getVolumeControls = function() {
// On Watch page slider has no ID, on Montage page it has ID
return (document.getElementById('volumeControls')) ? document.getElementById('volumeControls') : document.getElementById('volumeControls'+this.id);
return getVolumeControls(this.id);
};

this.getVolumeSlider = function() {
// On Watch page slider has no ID, on Montage page it has ID
return (document.getElementById('volumeSlider')) ? document.getElementById('volumeSlider') : document.getElementById('volumeSlider'+this.id);
return getVolumeSlider(this.id);
};

this.getIconMute = function() {
// On Watch page icon has no ID, on Montage page it has ID
return (document.getElementById('controlMute')) ? document.getElementById('controlMute') : document.getElementById('controlMute'+this.id);
return getIconMute(this.id);
};

this.getAVStream = function() {
Expand Down Expand Up @@ -1051,8 +1059,9 @@ function MonitorStream(monitorData) {
const volumeSlider = this.getVolumeSlider();
const audioStream = this.getAVStream();
if (!volumeSlider || !audioStream) return;
const iconMute = this.getIconMute();
$j('#volumeControls'+this.id).show();

//$j('#volumeControls'+this.id).show();
$j('#volumeControls'+this.id).removeClass('disabled');
if (!this.handlerEventListener['volumechange']) {
this.handlerEventListener['volumechange'] = manageEventListener.addEventListener(audioStream, 'volumechange',
(event) => {
Expand All @@ -1061,155 +1070,56 @@ function MonitorStream(monitorData) {
);
}
if (volumeSlider.noUiSlider) return;
const defaultVolume = (volumeSlider.getAttribute("data-volume") || 50);

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) {
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'");
});

if (volumeSlider.getAttribute("data-muted") !== "true") {
this.controlMute('off');
} else {
this.controlMute('on');
}
createVolumeSlider(volumeSlider, audioStream);

//if (volumeSlider.getAttribute("data-muted") !== "true") {
// this.controlMute('off');
//} else {
// this.controlMute('on');
//}
};

this.destroyVolumeSlider = function() {
$j('#volumeControls'+this.id).hide();
//$j('#volumeControls'+this.id).hide();
$j('#volumeControls'+this.id).addClass('disabled');
const volumeSlider = this.getVolumeSlider();
destroyVolumeSlider(volumeSlider);
//const iconMute = this.getIconMute();
//if (iconMute) iconMute.innerText = "";
if (volumeSlider && volumeSlider.noUiSlider) {
volumeSlider.noUiSlider.destroy();
volumeSlider.noUiSlider = null;
}
};

/*
* volume: on || off
*/
this.changeStateIconMute = function(volume) {
const volumeControls = this.getVolumeControls();
const disabled = (volumeControls) ? volumeControls.classList.contains('disabled') : false;
const iconMute = this.getIconMute();
if (!disabled && iconMute) {
iconMute.innerHTML = (volume == 'on')? 'volume_up' : 'volume_off';
}
return iconMute;
return changeStateIconMute(this.id, volume);
};

/*
* volume: on || off
*/
this.changeVolumeSlider = function(volume) {
const volumeControls = this.getVolumeControls();
//const controlMute = document.querySelector('[id ^= "controlMute'+this.id+'"]');
const volumeSlider = this.getVolumeSlider();

if (volumeSlider) {
let disabled = false;
if (volumeControls) {
disabled = volumeControls.classList.contains('disabled');
}
if (volume == 'on') {
volumeSlider.classList.remove('noUi-mute');
} else if (volume == 'off') {
volumeSlider.classList.add('noUi-mute');
}
if (volumeSlider.noUiSlider) {
(disabled) ? volumeSlider.noUiSlider.disable() : volumeSlider.noUiSlider.enable();
}
}
return volumeSlider;
return changeVolumeSlider(this.id, volume);
};

/*
* mode: switch, on, off
*/
this.controlMute = function(mode = 'switch') {
let volumeSlider = this.getVolumeSlider();
const audioStream = this.getAVStream();
const volumeControls = this.getVolumeControls();
const disabled = (volumeControls) ? volumeControls.classList.contains('disabled') : false;

if (volumeSlider && volumeSlider.noUiSlider) {
(disabled) ? volumeSlider.noUiSlider.disable() : volumeSlider.noUiSlider.enable();
}

if (disabled) {
console.log(`Volume control is disabled in controlMute for monitor ID=${this.id}`);
return;
}
if (!audioStream) {
console.log(`No audiostream! in controlMute for monitor ID=${this.id}`);
return;
}

if (mode=='switch') {
if (audioStream.muted) {
audioStream.muted = this.muted = false;
this.changeStateIconMute('on');
volumeSlider = this.changeVolumeSlider('on');
if (volumeSlider && volumeSlider.noUiSlider) {
audioStream.volume = volumeSlider.noUiSlider.get() / 100;
controlMute(this.id, mode);
if (audioStream) {
if (mode=='switch') {
if (audioStream.muted) {
this.muted = false;
} else {
this.muted = true;
}
} else {
audioStream.muted = this.muted = true;
this.changeStateIconMute('off');
this.changeVolumeSlider('off');
}
} else if (mode=='on') {
audioStream.muted = this.muted = true;
this.changeStateIconMute('off');
this.changeVolumeSlider('off');
} else if (mode=='off') {
audioStream.muted = this.muted = false;
this.changeStateIconMute('on');
volumeSlider = this.changeVolumeSlider('on');
if (volumeSlider && volumeSlider.noUiSlider) {
audioStream.volume = volumeSlider.noUiSlider.get() / 100;
} else if (mode=='on') {
this.muted = true;
} else if (mode=='off') {
this.muted = false;
}
}
};
Expand Down
5 changes: 5 additions & 0 deletions web/lang/en_gb.php
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,11 @@ function zmVlang($langVarArray, $count) {
is set to "my_camera", access the stream at rtsp://ZM_HOST:20006/my_camera
',
),
'OPTIONS_WHATTODISPLAY' => array(
'Help' => '
On the Watch, Montage, Event page, you can display either a video stream, or an audio stream visualization, or both a video stream and an audio visualization.
',
),
'FUNCTION_ANALYSIS_ENABLED' => array(
'Help' => '
When to perform motion detection on the captured video.
Expand Down
Loading
Loading