From 83f443c1248e9386b42cf2834542e9e8799419bf Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Sat, 25 Apr 2026 23:49:46 +0300 Subject: [PATCH 01/10] Upload audioMotionAnalyzer.js to the Monitor page (functions.php) This is necessary to determine whether we are using the analyzer, and if so, what version it is. We will then provide the user with useful information when they are in the Viewing tab. --- web/skins/classic/includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/skins/classic/includes/functions.php b/web/skins/classic/includes/functions.php index f031fbff7..e41033ba0 100644 --- a/web/skins/classic/includes/functions.php +++ b/web/skins/classic/includes/functions.php @@ -1816,7 +1816,7 @@ function xhtmlFooter() { $skinJsFile = getSkinFile('js/skin.js'); ?> - + From 446220cbf06606bdb14d2fa7bff6a3301c221ea5 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Sun, 26 Apr 2026 23:35:05 +0300 Subject: [PATCH 02/10] - Added support for "~~" =>
for translation - Now we only specify the supported analyzer version in one place in the audioMotionAnalyzer.js file - Added translations for three cases: when the analyzer is not installed, the wrong version is installed, or everything is fine. --- web/includes/lang.php | 4 +-- web/lang/en_gb.php | 8 +++--- web/lang/ru_ru.php | 8 +++--- .../assets/audioMotion-analyzer/src/help.txt | 3 +-- web/skins/classic/js/audioMotionAnalyzer.js | 14 ++++++---- web/skins/classic/views/js/monitor.js | 26 +++++++++++++++++++ web/skins/classic/views/js/monitor.js.php | 3 +++ web/skins/classic/views/monitor.php | 5 ++-- 8 files changed, 54 insertions(+), 17 deletions(-) diff --git a/web/includes/lang.php b/web/includes/lang.php index 60e569a7e..ccdd3d6ec 100644 --- a/web/includes/lang.php +++ b/web/includes/lang.php @@ -22,9 +22,9 @@ function translate($name) { global $SLANG; // The isset is more performant if ( isset($SLANG[$name]) || array_key_exists($name, $SLANG) ) - return $SLANG[$name]; + return preg_replace('/~~/', '
', $SLANG[$name]); else - return $name; + return preg_replace('/~~/', '
', $name); } function loadLanguage($prefix='') { diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 8305dcdcb..686daa3fe 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -121,6 +121,9 @@ $SLANG = array( 'AttrTotalScore' => 'Total Score', 'AttrStartWeekday' => 'Start Weekday', 'AttrEndWeekday' => 'End Weekday', + 'AudioMotionVersionOK' => 'Correct version installed "{AudioMotionVersionInstalled}"', + 'AudioMotionVersionNotInstalled' => 'Requires audio motion analyzer version "{AudioMotionVersionRequired}" to be installed~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', + 'AudioMotionVersionWrongVersion' => 'The required analyzer version is "{AudioMotionVersionRequired}", but you have it installed "{AudioMotionVersionInstalled}"~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', 'Auth' => 'Authentication', 'AutoStopTimeout' => 'Auto Stop Timeout', 'AvgBrScore' => 'Avg.
Score', @@ -1141,9 +1144,8 @@ None: No frames will be decoded, live view and thumbnails will not be available~ Audio motion visualization can be displayed on the Montage, Watch, and Event pages.~~ To do this, install the file "/skins/MySkin/assets/audioMotion-analyzer/src/audioMotion-analyzer.js".~~ This file can be downloaded from the following links:~~ - https://cdn.jsdelivr.net/npm/audiomotion-analyzer@4.5.4~~ - https://github.com/hvianna/audioMotion-analyzer/releases~~ - Currently supported version 4.5.4 + https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X where X.X.X is the version number~~ + https://github.com/hvianna/audioMotion-analyzer/releases ' ), 'ZM_OPT_TRAINING' => array( diff --git a/web/lang/ru_ru.php b/web/lang/ru_ru.php index 29d7bc07f..14cdfcb95 100644 --- a/web/lang/ru_ru.php +++ b/web/lang/ru_ru.php @@ -136,6 +136,9 @@ $SLANG = array( 'AttrStorageServer' => 'Сервер для размещения хранилища', // Added - 2018-08-30 'AttrSystemLoad' => 'Нагрузка проц.', 'AttrTotalScore' => 'Сумм. оценка', + 'AudioMotionVersionOK' => 'Установлена корректная версия "{AudioMotionVersionInstalled}"', + 'AudioMotionVersionNotInstalled' => 'Требуется установка audio motion analyzer версии "{AudioMotionVersionRequired}"~~Ссылка для загрузки: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} или https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', + 'AudioMotionVersionWrongVersion' => 'Требуется версия анализатора "{AudioMotionVersionRequired}", но у Вас установлена "{AudioMotionVersionInstalled}"~~Ссылка для загрузки: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} или https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', 'Auth' => 'Авторизация', 'Auto' => 'Авто', 'AutoStopTimeout' => 'Тайм-аут автоостановки', @@ -1200,9 +1203,8 @@ $OLANG = array( На страницах живого просмотра, монтажа и просмотра события возможно отображение движения аудио.~~ Для этого необходимо установить файл "/skins/MySkin/assets/audioMotion-analyzer/src/audioMotion-analyzer.js".~~ Указанный файл можно скачать по ссылкам:~~ - https://cdn.jsdelivr.net/npm/audiomotion-analyzer@4.5.4~~ - https://github.com/hvianna/audioMotion-analyzer/releases~~ - На данный момент поддердивается версия 4.5.4 + https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X где X.X.X это номер версии~~ + https://github.com/hvianna/audioMotion-analyzer/releases ' ), 'FUNCTION_ANALYSIS_ENABLED' => array( diff --git a/web/skins/classic/assets/audioMotion-analyzer/src/help.txt b/web/skins/classic/assets/audioMotion-analyzer/src/help.txt index bcc08737d..b3a0eb5d7 100644 --- a/web/skins/classic/assets/audioMotion-analyzer/src/help.txt +++ b/web/skins/classic/assets/audioMotion-analyzer/src/help.txt @@ -1,6 +1,5 @@ Audio motion visualization can be displayed on the Montage, Watch, and Event pages. To do this, install the file "/skins/MySkin/assets/audioMotion-analyzer/src/audioMotion-analyzer.js". This file can be downloaded from the following links: -https://cdn.jsdelivr.net/npm/audiomotion-analyzer@4.5.4 +https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X where X.X.X is the version number https://github.com/hvianna/audioMotion-analyzer/releases -Currently, version 4.5.4 is supported. diff --git a/web/skins/classic/js/audioMotionAnalyzer.js b/web/skins/classic/js/audioMotionAnalyzer.js index bbab8df51..9546b4594 100644 --- a/web/skins/classic/js/audioMotionAnalyzer.js +++ b/web/skins/classic/js/audioMotionAnalyzer.js @@ -3,6 +3,8 @@ * IgorA100 2026 */ +window.SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION = '4.5.4'; + var AudioMotionAnalyzer = null; function checkAudioMotionEnabled() { @@ -16,7 +18,13 @@ if (checkAudioMotionEnabled()) { } else { AudioMotionAnalyzer = window.AudioMotionAnalyzer; } + window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION = AudioMotionAnalyzer.version; + if (currentView == 'watch' || currentView == 'montage' || currentView == 'event') { + customElements.define('audio-motion', _AudioMotionAnalyzer); + } }); +} else { + window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION = "NotInstalled"; } //import {AudioMotionAnalyzer} from '../assets/audioMotion-analyzer/src/audioMotion-analyzer.js'; @@ -366,8 +374,4 @@ export class _AudioMotionAnalyzer extends HTMLElement { } return currentPlayer; }; -} // END CLASS - -if (checkAudioMotionEnabled() && (currentView == 'watch' || currentView == 'montage' || currentView == 'event')) { - customElements.define('audio-motion', _AudioMotionAnalyzer); -} +} // END CLASS \ No newline at end of file diff --git a/web/skins/classic/views/js/monitor.js b/web/skins/classic/views/js/monitor.js index 944079443..9c6ec93dd 100644 --- a/web/skins/classic/views/js/monitor.js +++ b/web/skins/classic/views/js/monitor.js @@ -528,8 +528,34 @@ function initPage() { if (!isMobile()) initThumbAnimation(); manageChannelStream(); + checkVerAudioMotion(); } // end function initPage() +async function checkVerAudioMotion() { + await waitUntil(() => window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION, 20000); + const whatDisplayInfo = document.getElementById("WhatDisplayInfo"); + whatDisplayInfo.classList.remove("text-success"); + whatDisplayInfo.classList.remove("text-info"); + whatDisplayInfo.classList.remove("text-danger"); + + if (window.SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION === window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION) { + whatDisplayInfo.innerHTML = applyTemplateAudioMotionTranslation(audioMotionVersionOK); + whatDisplayInfo.classList.add("text-success"); + } else if (window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION === "NotInstalled") { + whatDisplayInfo.innerHTML = applyTemplateAudioMotionTranslation(audioMotionVersionNotInstalled); + whatDisplayInfo.classList.add("text-info"); + } else { //The versions do not match + whatDisplayInfo.innerHTML = applyTemplateAudioMotionTranslation(audioMotionVersionWrongVersion); + whatDisplayInfo.classList.add("text-danger"); + } +} + +function applyTemplateAudioMotionTranslation(str) { + str = str.replaceAll('{AudioMotionVersionInstalled}', window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION); + str = str.replaceAll('{AudioMotionVersionRequired}', window.SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION); + return str; +} + function saveMonitorData(href = '') { const alertBlock = $j("#alertSaveMonitorData"); const form_data = $j("#contentForm").serializeArray(); diff --git a/web/skins/classic/views/js/monitor.js.php b/web/skins/classic/views/js/monitor.js.php index 8e79d7336..0a3c5edb5 100644 --- a/web/skins/classic/views/js/monitor.js.php +++ b/web/skins/classic/views/js/monitor.js.php @@ -1,6 +1,9 @@ const hasOnvif = ; const defaultAspectRatio = ''; const messageSavingDataWhenLeavingPage = ''; +const audioMotionVersionOK = ''; +const audioMotionVersionNotInstalled = ''; +const audioMotionVersionWrongVersion = ''; WhatDisplay()); - else - echo '' . translate('RequiresAudioMotionEnabled') . ''; + else + echo '' . translate('RequiresAudioMotionEnabled') . ''; + echo '
'; ?>
  • From 518ba21251bd16bf670287b5d34feda40e05dc20 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Wed, 29 Apr 2026 14:47:55 +0300 Subject: [PATCH 03/10] Update web/lang/en_gb.php Oh, my bad English :( Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- web/lang/en_gb.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 686daa3fe..3a1baa386 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -123,7 +123,7 @@ $SLANG = array( 'AttrEndWeekday' => 'End Weekday', 'AudioMotionVersionOK' => 'Correct version installed "{AudioMotionVersionInstalled}"', 'AudioMotionVersionNotInstalled' => 'Requires audio motion analyzer version "{AudioMotionVersionRequired}" to be installed~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', - 'AudioMotionVersionWrongVersion' => 'The required analyzer version is "{AudioMotionVersionRequired}", but you have it installed "{AudioMotionVersionInstalled}"~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', + 'AudioMotionVersionWrongVersion' => 'The required analyzer version is "{AudioMotionVersionRequired}", but you have "{AudioMotionVersionInstalled}" installed~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', 'Auth' => 'Authentication', 'AutoStopTimeout' => 'Auto Stop Timeout', 'AvgBrScore' => 'Avg.
    Score', From f9df56c43ff3a2cd0a0c69fd8a3663eb98acc9aa Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Wed, 29 Apr 2026 17:27:12 +0300 Subject: [PATCH 04/10] Apply suggestion from @Copilot We check for the file's existence, and if it does, we set AUDIO_MOTION_ENABLED = true. Okay, let's do an additional check in case the import fails for some reason. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- web/skins/classic/js/audioMotionAnalyzer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/skins/classic/js/audioMotionAnalyzer.js b/web/skins/classic/js/audioMotionAnalyzer.js index 9546b4594..570067a9b 100644 --- a/web/skins/classic/js/audioMotionAnalyzer.js +++ b/web/skins/classic/js/audioMotionAnalyzer.js @@ -22,6 +22,9 @@ if (checkAudioMotionEnabled()) { if (currentView == 'watch' || currentView == 'montage' || currentView == 'event') { customElements.define('audio-motion', _AudioMotionAnalyzer); } + }).catch(error => { + console.error('Failed to load audioMotion-analyzer module:', error); + window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION = "LoadFailed"; }); } else { window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION = "NotInstalled"; From 8896bcfdc016ca542e43ae523205fa7953367c73 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Wed, 29 Apr 2026 18:05:01 +0300 Subject: [PATCH 05/10] Abort checkVerAudioMotion() if we couldn't get the CURRENT_AUDIO_MOTION_ANALYZER_VERSION value (monitor.js) https://github.com/ZoneMinder/zoneminder/pull/4753/changes#r3158319956 --- web/skins/classic/views/js/monitor.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/skins/classic/views/js/monitor.js b/web/skins/classic/views/js/monitor.js index 9c6ec93dd..43612d566 100644 --- a/web/skins/classic/views/js/monitor.js +++ b/web/skins/classic/views/js/monitor.js @@ -532,7 +532,11 @@ function initPage() { } // end function initPage() async function checkVerAudioMotion() { - await waitUntil(() => window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION, 20000); + const result = await waitUntil(() => window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION, 20000); + if (result === false) { + console.warn("Unable to obtain the current version number of audio motion analyzer."); + return; + } const whatDisplayInfo = document.getElementById("WhatDisplayInfo"); whatDisplayInfo.classList.remove("text-success"); whatDisplayInfo.classList.remove("text-info"); From db910df5636db1a9c5c66e392f9fdc7601cf5ec7 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Wed, 29 Apr 2026 18:09:18 +0300 Subject: [PATCH 06/10] customElements.define moved after the class declaration (audioMotionAnalyzer.js) --- web/skins/classic/js/audioMotionAnalyzer.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/skins/classic/js/audioMotionAnalyzer.js b/web/skins/classic/js/audioMotionAnalyzer.js index 570067a9b..5b54b79f4 100644 --- a/web/skins/classic/js/audioMotionAnalyzer.js +++ b/web/skins/classic/js/audioMotionAnalyzer.js @@ -19,9 +19,6 @@ if (checkAudioMotionEnabled()) { AudioMotionAnalyzer = window.AudioMotionAnalyzer; } window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION = AudioMotionAnalyzer.version; - if (currentView == 'watch' || currentView == 'montage' || currentView == 'event') { - customElements.define('audio-motion', _AudioMotionAnalyzer); - } }).catch(error => { console.error('Failed to load audioMotion-analyzer module:', error); window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION = "LoadFailed"; @@ -377,4 +374,8 @@ export class _AudioMotionAnalyzer extends HTMLElement { } return currentPlayer; }; -} // END CLASS \ No newline at end of file +} // END CLASS + +if (checkAudioMotionEnabled() && (currentView == 'watch' || currentView == 'montage' || currentView == 'event')) { + customElements.define('audio-motion', _AudioMotionAnalyzer); +} From 19ff4caf60629d9bc9c593c9fb91cc0758fcc407 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Thu, 30 Apr 2026 13:43:21 +0300 Subject: [PATCH 07/10] Added replaceDoubleTildeToBR() & createClickableLink() function (skin.js) https://github.com/ZoneMinder/zoneminder/pull/4753#discussion_r3158319968 https://github.com/ZoneMinder/zoneminder/pull/4753#discussion_r3158319976 --- web/skins/classic/js/skin.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index e839dcdbe..55f298802 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -3130,6 +3130,15 @@ const getAVStream = function(mid) { return (document.querySelector('#liveStream'+mid + ' video') || document.getElementById('liveStream'+mid)); }; +const replaceDoubleTildeToBR = function(str) { + return (str.replaceAll("~~", "
    ")); +} + +const createClickableLink = function(text) { + const text1=text.replace(/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig, '$1'); + return text1.replace(/(^|[^\/])(www\.[\S]+(\b|$))/gim, '$1$2'); +} + // https://stackoverflow.com/a/69273090 class ManageEventListener { #listeners = {}; // # in a JS class signifies private From b219d3b02380587ba330330dea38050b2874eea0 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Thu, 30 Apr 2026 13:44:45 +0300 Subject: [PATCH 08/10] Revert (lang.php) --- web/includes/lang.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/includes/lang.php b/web/includes/lang.php index ccdd3d6ec..60e569a7e 100644 --- a/web/includes/lang.php +++ b/web/includes/lang.php @@ -22,9 +22,9 @@ function translate($name) { global $SLANG; // The isset is more performant if ( isset($SLANG[$name]) || array_key_exists($name, $SLANG) ) - return preg_replace('/~~/', '
    ', $SLANG[$name]); + return $SLANG[$name]; else - return preg_replace('/~~/', '
    ', $name); + return $name; } function loadLanguage($prefix='') { From d82a90b6a786b97f886815730d37a55edd0ba673 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Thu, 30 Apr 2026 13:48:53 +0300 Subject: [PATCH 09/10] In applyTemplateAudioMotionTranslation() , execute createClickableLink() & replaceDoubleTildeToBR() (monitor.js) --- web/skins/classic/views/js/monitor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/skins/classic/views/js/monitor.js b/web/skins/classic/views/js/monitor.js index 43612d566..4d44db985 100644 --- a/web/skins/classic/views/js/monitor.js +++ b/web/skins/classic/views/js/monitor.js @@ -557,7 +557,7 @@ async function checkVerAudioMotion() { function applyTemplateAudioMotionTranslation(str) { str = str.replaceAll('{AudioMotionVersionInstalled}', window.CURRENT_AUDIO_MOTION_ANALYZER_VERSION); str = str.replaceAll('{AudioMotionVersionRequired}', window.SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION); - return str; + return createClickableLink(replaceDoubleTildeToBR(str)); } function saveMonitorData(href = '') { From d01ad984df0daa32b073001cfe0adf7aff7efa25 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Thu, 30 Apr 2026 13:52:44 +0300 Subject: [PATCH 10/10] Fix: Eslint (skin.js) --- web/skins/classic/js/skin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index 55f298802..1c2e125a6 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -3132,12 +3132,12 @@ const getAVStream = function(mid) { const replaceDoubleTildeToBR = function(str) { return (str.replaceAll("~~", "
    ")); -} +}; const createClickableLink = function(text) { const text1=text.replace(/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig, '$1'); return text1.replace(/(^|[^\/])(www\.[\S]+(\b|$))/gim, '$1$2'); -} +}; // https://stackoverflow.com/a/69273090 class ManageEventListener {