From 3241fa59c5eb3834add68e537e90b69d985ef9d4 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 18 Jan 2022 12:52:21 -0500 Subject: [PATCH 01/48] Don't redirect if there was an error so that we display it --- web/includes/actions/monitor.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/includes/actions/monitor.php b/web/includes/actions/monitor.php index 2d900433a..7ec354179 100644 --- a/web/includes/actions/monitor.php +++ b/web/includes/actions/monitor.php @@ -324,7 +324,8 @@ if ($action == 'save') { // really should thump zmwatch and maybe zmtrigger too. //daemonControl( 'restart', 'zmwatch.pl' ); } // end if restart - $redirect = '?view=console'; + if (!$error_message) + $redirect = '?view=console'; } else { ZM\Warning("Unknown action $action in Monitor"); } // end if action == Delete From d2d9721c4e6ead2edbfab8c8acdbf5a87c2d11a1 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 18 Jan 2022 13:43:56 -0500 Subject: [PATCH 02/48] spacing, remove redundant debug --- src/zm_eventstream.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/zm_eventstream.cpp b/src/zm_eventstream.cpp index 54f6b60d8..28a0e50d6 100644 --- a/src/zm_eventstream.cpp +++ b/src/zm_eventstream.cpp @@ -1096,35 +1096,33 @@ void EventStream::runStream() { bool EventStream::send_file(const std::string &filepath) { FILE *fdj = nullptr; fdj = fopen(filepath.c_str(), "rb"); - if ( !fdj ) { + if (!fdj) { Error("Can't open %s: %s", filepath.c_str(), strerror(errno)); std::string error_message = stringtf("Can't open %s: %s", filepath.c_str(), strerror(errno)); return sendTextFrame(error_message.c_str()); } #if HAVE_SENDFILE static struct stat filestat; - if ( fstat(fileno(fdj), &filestat) < 0 ) { + if (fstat(fileno(fdj), &filestat) < 0) { fclose(fdj); /* Close the file handle */ Error("Failed getting information about file %s: %s", filepath.c_str(), strerror(errno)); return false; } - if ( !filestat.st_size ) { + if (!filestat.st_size) { fclose(fdj); /* Close the file handle */ Info("File size is zero. Unable to send raw frame %ld: %s", curr_frame_id, strerror(errno)); return false; } - if ( 0 > fprintf(stdout, "Content-Length: %d\r\n\r\n", (int)filestat.st_size) ) { + if (0 > fprintf(stdout, "Content-Length: %d\r\n\r\n", (int)filestat.st_size)) { fclose(fdj); /* Close the file handle */ Info("Unable to send raw frame %ld: %s", curr_frame_id, strerror(errno)); return false; } int rc = zm_sendfile(fileno(stdout), fileno(fdj), 0, (int)filestat.st_size); - if ( rc == (int)filestat.st_size ) { + if (rc == (int)filestat.st_size) { // Success fclose(fdj); /* Close the file handle */ return true; - } else { - Debug(1, "Failed to sendfile?"); } Warning("Unable to send raw frame %ld: %s rc %d != %d", curr_frame_id, strerror(errno), rc, (int)filestat.st_size); From 13ee39b1e9fb8b4103acb0a71e71d71b13031c4b Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 18 Jan 2022 13:44:21 -0500 Subject: [PATCH 03/48] Log errors talking to Janus. Log if janus is turned on but not compiled in --- src/zm_monitor.cpp | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index aea0c0000..454e7068c 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1126,6 +1126,9 @@ bool Monitor::connect() { } } } +#else + if (janus_enabled) + Error("zmc not compiled with LIBCURL. Janus support not built in!"); #endif } else if (!shared_data->valid) { @@ -3390,7 +3393,10 @@ int Monitor::add_to_janus() { CURLcode res; curl = curl_easy_init(); - if(!curl) return -1; + if (!curl) { + Error("Failed to init curl"); + return -1; + } //parse username and password pos = path.find(":", 7); if (pos == std::string::npos) return -1; @@ -3408,7 +3414,11 @@ int Monitor::add_to_janus() { curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); res = curl_easy_perform(curl); - if (res != CURLE_OK) return -1; + if (res != CURLE_OK) { + Error("Failed to curl_easy_perform getting session/handle id"); + curl_easy_cleanup(curl); + return -1; + } pos = response.find("\"id\": "); if (pos == std::string::npos) return -1; @@ -3421,7 +3431,11 @@ int Monitor::add_to_janus() { curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); res = curl_easy_perform(curl); - if (res != CURLE_OK) return -1; + if (res != CURLE_OK) { + Error("Failed to curl_easy_perform attaching"); + curl_easy_cleanup(curl); + return -1; + } pos = response.find("\"id\": "); if (pos == std::string::npos) return -1; std::string handle_id = response.substr(pos + 6, 16); //TODO: This is an assumption that the string is always 16 @@ -3446,7 +3460,11 @@ int Monitor::add_to_janus() { curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); res = curl_easy_perform(curl); - if (res != CURLE_OK) return -1; + if (res != CURLE_OK) { + Error("Failed to curl_easy_perform adding rtsp stream"); + curl_easy_cleanup(curl); + return -1; + } Debug(1,"Added stream to Janus: %s", response.c_str()); curl_easy_cleanup(curl); return 0; From 388735e94288c65914f8ed60eb007fab987b759b Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 18 Jan 2022 14:30:10 -0500 Subject: [PATCH 04/48] Fix relating auth_hash causing repeated reloads. If https, then assume a reverse proxy setup to janus. If video feed is not an img, log it and return --- web/js/MonitorStream.js | 47 +++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index 5a0dd7e25..54669a964 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -47,6 +47,11 @@ function MonitorStream(monitorData) { this.scale = newscale; const oldSrc = img.getAttribute('src'); + if (!oldSrc) { + console.log("No src on img?!"); + console.log(img); + return; + } let newSrc = ''; img.setAttribute('src', ''); @@ -85,7 +90,14 @@ function MonitorStream(monitorData) { if (this.janusEnabled) { var id = parseInt(this.id); - var server = "http://" + window.location.hostname + ":8088/janus"; + var server; + if (window.location.protocol=='https:') { + // Assume reverse proxy setup for now + server = "https://" + window.location.hostname + "/janus"; + } else { + server = "http://" + window.location.hostname + ":8088/janus"; + } + if (janus == null) { Janus.init({debug: "all", callback: function() { janus = new Janus({server: server}); //new Janus @@ -178,11 +190,16 @@ function MonitorStream(monitorData) { this.getStreamCmdResponse = function(respObj, respText) { var stream = $j('#liveStream'+this.id)[0]; - if ( ! stream ) { + if (!stream) { console.log('No live stream'); return; } + //watchdogOk('stream'); + if (streamCmdTimer) { + streamCmdTimer = clearTimeout(streamCmdTimer); + } + if ( respObj.result == 'Ok' ) { if ( respObj.status ) { this.status = respObj.status; @@ -202,13 +219,25 @@ function MonitorStream(monitorData) { !COMPACT_MONTAGE) && (this.type != 'WebSite') ) { - var fpsValue = $j('#fpsValue'+this.id); - var stateValue = $j('#stateValue'+this.id); - var monitorState = $j('#monitorState'+this.id); + const viewingFPSValue = $j('#vewingFPSValue'+this.id); + const captureFPSValue = $j('#captureFPSValue'+this.id); + const analysisFPSValue = $j('#analysisFPSValue'+this.id); - if ( fpsValue.length ) fpsValue.text(this.status.fps); - if ( stateValue.length ) stateValue.text(stateStrings[this.alarmState]); - if ( monitorState.length ) this.setStateClass(monitorState, stateClass); + const stateValue = $j('#stateValue'+this.id); + const monitorState = $j('#monitorState'+this.id); + + if (viewingFPSValue.length && (viewingFPSValue.text != this.status.fps)) { + viewingFPSValue.text(this.status.fps); + } + if (analysisFPSValue.length && (analysisFPSValue.text != this.status.analysisfps)) { + analysisFPSValue.text(this.status.analysisfps); + } + if (captureFPSValue.length && (captureFPSValue.text != this.status.capturefps)) { + captureFPSValue.text(this.status.capturefps); + } + + if (stateValue.length) stateValue.text(stateStrings[this.alarmState]); + if (monitorState.length) this.setStateClass(monitorState, stateClass); } this.setStateClass($j('#monitor'+this.id), stateClass); @@ -238,7 +267,7 @@ function MonitorStream(monitorData) { } } if (this.status.auth) { - if (this.status.auth != auth_hash) { + if (this.status.auth != this.auth_hash) { // Try to reload the image stream. if (stream) { const oldsrc = stream.src; From d00aaa11e9231e0d6d96adde5e327307e61ab6f2 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 18 Jan 2022 23:01:24 -0500 Subject: [PATCH 05/48] default JanusEnabled to 0 so that we can turn it off --- web/includes/actions/monitor.php | 1 + 1 file changed, 1 insertion(+) diff --git a/web/includes/actions/monitor.php b/web/includes/actions/monitor.php index 7ec354179..e0f695d44 100644 --- a/web/includes/actions/monitor.php +++ b/web/includes/actions/monitor.php @@ -89,6 +89,7 @@ if ($action == 'save') { 'ModectDuringPTZ' => 0, 'Enabled' => 0, 'DecodingEnabled' => 0, + 'JanusEnabled' => 0, 'Exif' => 0, 'RTSPDescribe' => 0, 'V4LMultiBuffer' => '', From fb832e7d1bd13b645fed9bd26c994d2fa5278788 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 19 Jan 2022 00:14:52 -0600 Subject: [PATCH 06/48] Adds janus_enable_audio, a switch to try to enable audio in live stream viewing. --- db/zm_create.sql.in | 1 + db/zm_update-1.37.9.sql | 18 ++++++++++++++++++ src/zm_monitor.cpp | 9 ++++++--- src/zm_monitor.h | 13 +++++++++++++ web/ajax/modals/function.php | 10 ++++++++++ web/includes/Monitor.php | 1 + web/includes/actions/monitor.php | 1 + web/lang/en_gb.php | 5 +++++ web/skins/classic/views/js/monitor.js | 15 +++++++++++++++ web/skins/classic/views/monitor.php | 10 ++++++++++ 10 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 db/zm_update-1.37.9.sql diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index 06d2ab385..db72cb58d 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -456,6 +456,7 @@ CREATE TABLE `Monitors` ( `Enabled` tinyint(3) unsigned NOT NULL default '1', `DecodingEnabled` tinyint(3) unsigned NOT NULL default '1', `JanusEnabled` BOOLEAN NOT NULL default false, + `JanusAudioEnabled` BOOLEAN NOT NULL default false, `LinkedMonitors` varchar(255), `Triggers` set('X10') NOT NULL default '', `EventStartCommand` VARCHAR(255) NOT NULL DEFAULT '', diff --git a/db/zm_update-1.37.9.sql b/db/zm_update-1.37.9.sql new file mode 100644 index 000000000..be7d2a9ec --- /dev/null +++ b/db/zm_update-1.37.9.sql @@ -0,0 +1,18 @@ +-- +-- Update Monitors table to have JanusEnabled +-- + +SELECT 'Checking for JanusAudioEnabled in Monitors'; +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Monitors' + AND table_schema = DATABASE() + AND column_name = 'JanusAudioEnabled' + ) > 0, +"SELECT 'Column JanusAudioEnabled already exists in Monitors'", +"ALTER TABLE `Monitors` ADD COLUMN `JanusAudioEnabled` BOOLEAN NOT NULL default false AFTER `JanusEnabled`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 454e7068c..1733dd24d 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -77,7 +77,7 @@ struct Namespace namespaces[] = // This is the official SQL (and ordering of the fields) to load a Monitor. // It will be used whereever a Monitor dbrow is needed. WHERE conditions can be appended std::string load_monitor_sql = -"SELECT `Id`, `Name`, `ServerId`, `StorageId`, `Type`, `Function`+0, `Enabled`, `DecodingEnabled`, `JanusEnabled`," +"SELECT `Id`, `Name`, `ServerId`, `StorageId`, `Type`, `Function`+0, `Enabled`, `DecodingEnabled`, `JanusEnabled`, `JanusAudioEnabled`," "`LinkedMonitors`, `EventStartCommand`, `EventEndCommand`, `AnalysisFPSLimit`, `AnalysisUpdateDelay`, `MaxFPS`, `AlarmMaxFPS`," "`Device`, `Channel`, `Format`, `V4LMultiBuffer`, `V4LCapturesPerFrame`, " // V4L Settings "`Protocol`, `Method`, `Options`, `User`, `Pass`, `Host`, `Port`, `Path`, `SecondPath`, `Width`, `Height`, `Colours`, `Palette`, `Orientation`+0, `Deinterlacing`, " @@ -307,6 +307,7 @@ Monitor::Monitor() enabled(false), decoding_enabled(false), janus_enabled(false), + janus_audio_enabled(false), //protocol //method //options @@ -447,7 +448,7 @@ Monitor::Monitor() /* std::string load_monitor_sql = - "SELECT Id, Name, ServerId, StorageId, Type, Function+0, Enabled, DecodingEnabled, JanusEnabled, LinkedMonitors, `EventStartCommand`, `EventEndCommand`, " + "SELECT Id, Name, ServerId, StorageId, Type, Function+0, Enabled, DecodingEnabled, JanusEnabled, JanusAudioEnabled, LinkedMonitors, `EventStartCommand`, `EventEndCommand`, " "AnalysisFPSLimit, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS," "Device, Channel, Format, V4LMultiBuffer, V4LCapturesPerFrame, " // V4L Settings "Protocol, Method, Options, User, Pass, Host, Port, Path, SecondPath, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, RTSPDescribe, " @@ -501,6 +502,7 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { decoding_enabled = dbrow[col] ? atoi(dbrow[col]) : false; col++; // See below after save_jpegs for a recalculation of decoding_enabled janus_enabled = dbrow[col] ? atoi(dbrow[col]) : false; col++; + janus_audio_enabled = dbrow[col] ? atoi(dbrow[col]) : false; col++; ReloadLinkedMonitors(dbrow[col]); col++; event_start_command = dbrow[col] ? dbrow[col] : ""; col++; @@ -1121,7 +1123,7 @@ bool Monitor::connect() { #if HAVE_LIBCURL //janus setup. Depends on libcurl. if (janus_enabled && (path.find("rtsp://") != std::string::npos)) { if (add_to_janus() != 0) { - if (add_to_janus() != 0) { + if (add_to_janus() != 0) { //The initial attempt may fail. This is a temporary workaround. Warning("Failed to add monitor stream to Janus!"); } } @@ -3453,6 +3455,7 @@ int Monitor::add_to_janus() { postData += rtsp_password; postData += "\", \"id\" : "; postData += std::to_string(id); + if (janus_audio_enabled) postData += ", \"audio\" : true"; postData += ", \"video\" : true}}"; curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 0d63816e9..94c37affb 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -269,6 +269,7 @@ protected: bool enabled; // Whether the monitor is enabled or asleep bool decoding_enabled; // Whether the monitor will decode h264/h265 packets bool janus_enabled; // Whether we set the h264/h265 stream up on janus + bool janus_audio_enabled; // Whether we tell Janus to try to include audio. std::string protocol; std::string method; @@ -511,6 +512,18 @@ public: inline bool DecodingEnabled() const { return decoding_enabled; } + bool JanusEnabled() { + return janus_enabled; + } + bool JanusAudioEnabled() { + return janus_audio_enabled; + } + bool OnvifEnabled() { + return onvif_event_listener; + } + bool OnvifHealthy() { + return ONVIF_Healthy; + } inline const char *EventPrefix() const { return event_prefix.c_str(); } inline bool Ready() const { if ( image_count >= ready_count ) { diff --git a/web/ajax/modals/function.php b/web/ajax/modals/function.php index b23bbb649..c47664c4b 100644 --- a/web/ajax/modals/function.php +++ b/web/ajax/modals/function.php @@ -79,6 +79,16 @@ if ( !canEdit('Monitors') ) return; } ?> + +
+ + +'.$OLANG['FUNCTION_JANUS_AUDIO_ENABLED']['Help'].'
'; + } +?> + '; + } ?> From 04cfe372f83fbb70c56d7ff68674f327b80fda17 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 19 Jan 2022 00:56:36 -0600 Subject: [PATCH 07/48] Wrap public access function in Gsoap ifdef to remove build warning --- src/zm_monitor.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 94c37affb..898349eb3 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -521,9 +521,11 @@ public: bool OnvifEnabled() { return onvif_event_listener; } +#ifdef WITH_GSOAP bool OnvifHealthy() { return ONVIF_Healthy; } +#endif inline const char *EventPrefix() const { return event_prefix.c_str(); } inline bool Ready() const { if ( image_count >= ready_count ) { From ebcb3abf2efaca3e65cffa6acb5224f43cfa7da0 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 19 Jan 2022 09:33:30 -0500 Subject: [PATCH 08/48] image_count is only relevant to capture. decode and analysis should use packet->image_index for indexing into image_buffer, etc. --- src/zm_monitor.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 1733dd24d..f5d225d66 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -2076,7 +2076,7 @@ bool Monitor::Analyse() { && (event->Duration() >= min_section_length) && ((!pre_event_count) || (Event::PreAlarmCount() >= alarm_frame_count - 1))) { Info("%s: %03d - Closing event %" PRIu64 ", continuous end, alarm begins", - name.c_str(), image_count, event->Id()); + name.c_str(), snap->image_index, event->Id()); closeEvent(); } else if (event) { // This is so if we need more than 1 alarm frame before going into alarm, so it is basically if we have enough alarm frames @@ -2091,7 +2091,7 @@ bool Monitor::Analyse() { } if ((!pre_event_count) || (Event::PreAlarmCount() >= alarm_frame_count-1)) { Info("%s: %03d - Gone into alarm state PreAlarmCount: %u > AlarmFrameCount:%u Cause:%s", - name.c_str(), image_count, Event::PreAlarmCount(), alarm_frame_count, cause.c_str()); + name.c_str(), snap->image_index, Event::PreAlarmCount(), alarm_frame_count, cause.c_str()); shared_data->state = state = ALARM; } else if (state != PREALARM) { @@ -2214,7 +2214,7 @@ bool Monitor::Analyse() { || std::chrono::duration_cast(snap->timestamp.time_since_epoch()) % section_length == Seconds(0))) { Info("%s: %03d - Closing event %" PRIu64 ", section end forced %" PRIi64 " - %" PRIi64 " = %" PRIi64 " >= %" PRIi64 , name.c_str(), - image_count, + snap->image_index, event->Id(), static_cast(std::chrono::duration_cast(snap->timestamp.time_since_epoch()).count()), static_cast(std::chrono::duration_cast(event->StartTime().time_since_epoch()).count()), @@ -2558,8 +2558,6 @@ int Monitor::Capture() { } else { Debug(4, "Not Queueing audio packet"); } - // Don't update last_write_index because that is used for live streaming - //shared_data->last_write_time = image_buffer[index].timestamp->tv_sec; return 1; } else { Debug(1, "Unknown codec type %d", packet->codec_type); @@ -2690,7 +2688,7 @@ bool Monitor::Decode() { } // end if need_decoding Image* capture_image = nullptr; - unsigned int index = image_count % image_buffer_count; + unsigned int index = packet->image_index % image_buffer_count; if (packet->image) { capture_image = packet->image; From 062cde54a0c8139832aa4368eb75ddfae7aaa151 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 19 Jan 2022 10:45:38 -0500 Subject: [PATCH 09/48] Bump version for janus audio --- distros/redhat/zoneminder.spec | 2 +- version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/distros/redhat/zoneminder.spec b/distros/redhat/zoneminder.spec index 33996f755..16b8efb7e 100644 --- a/distros/redhat/zoneminder.spec +++ b/distros/redhat/zoneminder.spec @@ -36,7 +36,7 @@ %global _hardened_build 1 Name: zoneminder -Version: 1.37.8 +Version: 1.37.9 Release: 1%{?dist} Summary: A camera monitoring and analysis tool Group: System Environment/Daemons diff --git a/version b/version index 21b6230a1..938adc6f6 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.37.8 +1.37.9 From 05043a37b1a35bc7f2a6ca34c58b54148fcc76bf Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 19 Jan 2022 12:27:16 -0500 Subject: [PATCH 10/48] include image index in debug --- src/zm_monitorstream.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zm_monitorstream.cpp b/src/zm_monitorstream.cpp index f9e91d94b..a6232a23e 100644 --- a/src/zm_monitorstream.cpp +++ b/src/zm_monitorstream.cpp @@ -727,12 +727,12 @@ void MonitorStream::runStream() { (monitor->GetFunction() == Monitor::MOCORD || monitor->GetFunction() == Monitor::MODECT)) { Debug(1, "Sending analysis image"); send_image = monitor->GetAlarmImage(); - if ( !send_image ) { + if (!send_image) { Debug(1, "Falling back"); send_image = monitor->image_buffer[index]; } } else { - Debug(1, "Sending regular image"); + Debug(1, "Sending regular image index %d", index); send_image = monitor->image_buffer[index]; } From dec440ead1ba8b4b3c3baf17543e7ccd56e901d5 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 19 Jan 2022 15:01:37 -0500 Subject: [PATCH 11/48] Unset holdbuffer so that when we connect, we reset the shm buffer pointer. cleanup initializers in Image --- src/zm_image.cpp | 37 +++++++++++++++++++------------------ src/zm_image.h | 4 +++- src/zm_monitor.cpp | 1 + 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 1c58130cc..a4aad8b61 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -114,22 +114,23 @@ Image::Image() : delta8_argb(&std_delta8_argb), delta8_abgr(&std_delta8_abgr), delta8_gray8(&std_delta8_gray8), - blend(&std_blend) + blend(&std_blend), + width(0), + linesize(0), + height(0), + pixels(0), + colours(0), + padding(0), + size(0), + subpixelorder(0), + allocation(0), + buffer(nullptr), + buffertype(ZM_BUFTYPE_DONTFREE), + holdbuffer(0) { - if ( !initialised ) + if (!initialised) Initialise(); - width = 0; - linesize = 0; - height = 0; - padding = 0; - pixels = 0; - colours = 0; - subpixelorder = 0; - size = 0; - allocation = 0; - buffer = 0; - buffertype = ZM_BUFTYPE_DONTFREE; - holdbuffer = 0; + // Update blend to fast function determined by Initialise, I'm sure this can be improve. blend = fptr_blend; } @@ -158,15 +159,15 @@ Image::Image(int p_width, int p_height, int p_colours, int p_subpixelorder, uint colours(p_colours), padding(p_padding), subpixelorder(p_subpixelorder), - buffer(p_buffer) { + buffer(p_buffer), + holdbuffer(0) +{ if (!initialised) Initialise(); pixels = width * height; linesize = p_width * p_colours; size = linesize * height + padding; - buffer = nullptr; - holdbuffer = 0; if (p_buffer) { allocation = size; buffertype = ZM_BUFTYPE_DONTFREE; @@ -174,7 +175,7 @@ Image::Image(int p_width, int p_height, int p_colours, int p_subpixelorder, uint } else { AllocImgBuffer(size); } - if (!subpixelorder and colours>1) { + if (!subpixelorder and (colours>1)) { // Default to RGBA when no subpixelorder is specified. subpixelorder = ZM_SUBPIX_ORDER_RGBA; } diff --git a/src/zm_image.h b/src/zm_image.h index 74e5931eb..8bcb92c4c 100644 --- a/src/zm_image.h +++ b/src/zm_image.h @@ -145,11 +145,13 @@ class Image { explicit Image(const AVFrame *frame); ~Image(); + static void Initialise(); static void Deinitialise(); inline void DumpImgBuffer() { - DumpBuffer(buffer, buffertype); + if (buffertype != ZM_BUFTYPE_DONTFREE) + DumpBuffer(buffer, buffertype); buffertype = ZM_BUFTYPE_DONTFREE; buffer = nullptr; allocation = 0; diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index f5d225d66..2179197e8 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1153,6 +1153,7 @@ bool Monitor::disconnect() { } if (purpose == CAPTURE) { + alarm_image.HoldBuffer(false); /* Allow to reset buffer */ if (unlink(mem_file.c_str()) < 0) { Warning("Can't unlink '%s': %s", mem_file.c_str(), strerror(errno)); } From db866fa668c19e6ecaf6a4ee06200deefef25a15 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 20 Jan 2022 09:46:38 -0500 Subject: [PATCH 12/48] Implement zm_setcookie to simplify setting cookies, set samesite, deal with older php etc. Use it. --- web/includes/Group.php | 2 +- web/includes/session.php | 17 +++++++++++++++++ web/index.php | 17 ++--------------- web/skins/classic/views/montage.php | 9 +-------- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/web/includes/Group.php b/web/includes/Group.php index 34db0e869..93c20fc70 100644 --- a/web/includes/Group.php +++ b/web/includes/Group.php @@ -25,7 +25,7 @@ class Group extends ZM_Object { if ( isset($_COOKIE['zmGroup']) ) { if ( $this->{'Id'} == $_COOKIE['zmGroup'] ) { unset($_COOKIE['zmGroup']); - setcookie('zmGroup', '', time()-3600*24*2); + zm_setcookie('zmGroup', ''); } } } diff --git a/web/includes/session.php b/web/includes/session.php index 0190f9897..6e9c17670 100644 --- a/web/includes/session.php +++ b/web/includes/session.php @@ -1,4 +1,21 @@ =')) { + setcookie($cookie, $value, $options); + } else { + setcookie($cookie, $value, $options['expires'], '/; samesite=strict'); + } +} + // ZM session start function support timestamp management function zm_session_start() { diff --git a/web/index.php b/web/index.php index b3df502f6..5ace8e9b6 100644 --- a/web/index.php +++ b/web/index.php @@ -139,11 +139,6 @@ $skinBase[] = $skin; zm_session_start(); -$cookie_options = array( - 'expires'=>time()+3600*24*30*12*10, - 'samesite' => 'Strict', -); - if ( !isset($_SESSION['skin']) || isset($_REQUEST['skin']) || @@ -151,11 +146,7 @@ if ( ($_COOKIE['zmSkin'] != $skin) ) { $_SESSION['skin'] = $skin; - if (version_compare(phpversion(), '7.3.0', '>=')) { - setcookie('zmSkin', $skin, $cookie_options); - } else { - setcookie('zmSkin', $skin, $cookie_options['expires'], '/; samesite=strict'); - } + zm_setcookie('zmSkin', $skin); } if ( @@ -165,11 +156,7 @@ if ( ($_COOKIE['zmCSS'] != $css) ) { $_SESSION['css'] = $css; - if (version_compare(phpversion(), '7.3.0', '>=')) { - setcookie('zmCSS', $css, $cookie_options); - } else { - setcookie('zmCSS', $css, $cookie_options['expires'], '/; samesite=strict'); - } + zm_setcookie('zmCSS', $css); } # Running is global but only do the daemonCheck if it is actually needed diff --git a/web/skins/classic/views/montage.php b/web/skins/classic/views/montage.php index bb99916dd..a96e8a88e 100644 --- a/web/skins/classic/views/montage.php +++ b/web/skins/classic/views/montage.php @@ -307,14 +307,7 @@ foreach (array_reverse($zones) as $zone) { Type() != 'WebSite')) { -?> -
- : - -  -  -  fps -
-getMonitorStateHTML(); } ?> From a57206ef5493b5cfabbaf09caecfe9ebb333b8ea Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 20 Jan 2022 09:47:36 -0500 Subject: [PATCH 13/48] Implement getMonitorStateHTML to synchronize it between montage and live view, cycle etc. Reuseable code. Also the structure is required by MonitorStream.js --- web/includes/Monitor.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/web/includes/Monitor.php b/web/includes/Monitor.php index c8fa6ebb1..09d4657a8 100644 --- a/web/includes/Monitor.php +++ b/web/includes/Monitor.php @@ -719,5 +719,27 @@ class Monitor extends ZM_Object { } return $this->{'Manufacturer'}; } + function getMonitorStateHTML() { + $html = ' +
+
+ '.translate('State').': + fps + fps +'; + if ( $this->Function() == 'Modect' or $this->Function() == 'Mocord' ) { + $html .= ' fps + '; + } + $html .= ' + + + + '. translate('Zoom').': x +
+
+'; + return $html; + } } // end class Monitor ?> From 9395b7e47c27199d2bc4c88fecff8756af110747 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 20 Jan 2022 09:47:53 -0500 Subject: [PATCH 14/48] Use net zm_setcookie --- web/includes/actions/bandwidth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/includes/actions/bandwidth.php b/web/includes/actions/bandwidth.php index c5fafa1c9..ac6bd407f 100644 --- a/web/includes/actions/bandwidth.php +++ b/web/includes/actions/bandwidth.php @@ -21,7 +21,7 @@ if ( $action == 'bandwidth' && isset($_REQUEST['newBandwidth']) ) { $_COOKIE['zmBandwidth'] = validStr($_REQUEST['newBandwidth']); - setcookie('zmBandwidth', validStr($_REQUEST['newBandwidth']), time()+3600*24*30*12*10); + zm_setcookie('zmBandwidth', validStr($_REQUEST['newBandwidth'])); $refreshParent = true; $view = 'none'; $closePopup = true; From 3cd6fbdc12d0ec056683e8aaf2350c0bde495418 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 20 Jan 2022 09:48:02 -0500 Subject: [PATCH 15/48] Use net zm_setcookie --- web/includes/actions/groups.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/includes/actions/groups.php b/web/includes/actions/groups.php index 8e4522187..bc431998e 100644 --- a/web/includes/actions/groups.php +++ b/web/includes/actions/groups.php @@ -21,9 +21,9 @@ // Group view actions if ( ($action == 'setgroup') && canView('Groups')) { if ( !empty($_REQUEST['gid']) ) { - setcookie('zmGroup', validInt($_REQUEST['gid']), time()+3600*24*30*12*10); + zm_setcookie('zmGroup', validInt($_REQUEST['gid'])); } else { - setcookie('zmGroup', '', time()-3600*24*2); + zm_setcookie('zmGroup', '', time()-3600*24*2); } $refreshParent = true; return; From 4bf55b1af196fc777d95c4d6c42976c9005c46c7 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 20 Jan 2022 09:48:09 -0500 Subject: [PATCH 16/48] Use net zm_setcookie --- web/includes/actions/montage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/includes/actions/montage.php b/web/includes/actions/montage.php index cd9a41aaf..a5306a33b 100644 --- a/web/includes/actions/montage.php +++ b/web/includes/actions/montage.php @@ -38,7 +38,7 @@ if ( isset($_REQUEST['object']) ) { $Layout->save(); zm_session_start(); $_SESSION['zmMontageLayout'] = $Layout->Id(); - setcookie('zmMontageLayout', $Layout->Id(), 1); + zm_setcookie('zmMontageLayout', $Layout->Id()); session_write_close(); $redirect = '?view=montage'; } // end if save From 22b63377528404c7115f27c7e18a3a042b9fa9db Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 20 Jan 2022 11:47:28 -0500 Subject: [PATCH 17/48] Merge code from watch.js. Add an ajaxQueue. Link up buttons from UI --- web/js/MonitorStream.js | 368 ++++++++++++++++++++++++++++------------ 1 file changed, 264 insertions(+), 104 deletions(-) diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index 54669a964..17f43c4b9 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -13,15 +13,22 @@ function MonitorStream(monitorData) { this.janusEnabled = monitorData.janusEnabled; this.scale = 100; this.status = null; - this.alarmState = STATE_IDLE; this.lastAlarmState = STATE_IDLE; + this.streamCmdTimer = null; this.streamCmdParms = { view: 'request', request: 'stream', connkey: this.connKey }; + this.ajaxQueue = null; this.type = monitorData.type; this.refresh = monitorData.refresh; + + this.buttons = {}; // index by name + this.setButton = function(name, element) { + this.buttons.name = element; + }; + this.element = null; this.getElement = function() { if (this.element) return this.element; @@ -36,7 +43,7 @@ function MonitorStream(monitorData) { this.show = function() { const stream = this.getElement(); if (!stream.src) { - stream.src = this.url_to_zms+"&mode=single&scale=100&connkey="+this.connKey; + stream.src = this.url_to_zms+"&mode=single&scale=100&connkey="+this.connKey+this.auth_relay; } }; @@ -48,7 +55,7 @@ function MonitorStream(monitorData) { const oldSrc = img.getAttribute('src'); if (!oldSrc) { - console.log("No src on img?!"); + console.log('No src on img?!'); console.log(img); return; } @@ -58,13 +65,10 @@ function MonitorStream(monitorData) { console.log("Scaling to: " + newscale); if (newscale == '0' || newscale == 'auto') { - let bottomElement = document.getElementById('replayStatus'); - if (!bottomElement) { - bottomElement = document.getElementById('monitorState'); - } + const bottomElement = document.getElementById('monitorState'+this.id); var newSize = scaleToFit(this.width, this.height, $j(img), $j(bottomElement)); - console.log(newSize); + //console.log(newSize); newWidth = newSize.width; newHeight = newSize.height; autoScale = parseInt(newSize.autoScale); @@ -83,11 +87,8 @@ function MonitorStream(monitorData) { } img.setAttribute('src', newSrc); }; - this.start = function(delay) { - // Step 1 make sure we are streaming instead of a static image - const stream = this.getElement(); - if (!stream) return; + this.start = function(delay) { if (this.janusEnabled) { var id = parseInt(this.id); var server; @@ -106,47 +107,46 @@ function MonitorStream(monitorData) { attachVideo(id); return; } + + const stream = this.getElement(); + if (!stream) return; if (!stream.src) { - // Website Monitors won't have an img tag + // Website Monitors won't have an img tag, neither will video console.log('No src for #liveStream'+this.id); console.log(stream); return; } + // Step 1 make sure we are streaming instead of a static image src = stream.src.replace(/mode=single/i, 'mode=jpeg'); - if ( -1 == src.search('connkey') ) { + if (-1 == src.search('connkey')) { src += '&connkey='+this.connKey; } - if ( stream.src != src ) { + if (stream.src != src) { console.log("Setting to streaming: " + src); stream.src = ''; stream.src = src; } - setTimeout(this.streamCmdQuery.bind(this), delay); + setTimeout(this.statusQuery.bind(this), delay); }; + this.stop = function() { if ( 0 ) { - var stream = $j('#liveStream'+this.id)[0]; - if ( ! stream ) { - console.log('No live stream'); - return; - } + const stream = this.getElement(); + if (!stream) return; src = stream.src.replace(/mode=jpeg/i, 'mode=single'); - if ( stream.src != src ) { + if (stream.src != src) { console.log("Setting to stopped"); stream.src = ''; stream.src = src; } } - this.streamCmdParms.command = CMD_STOP; - this.streamCmdReq(this.streamCmdParms); + this.streamCommand(CMD_STOP); }; this.pause = function() { - this.streamCmdParms.command = CMD_PAUSE; - this.streamCmdReq(this.streamCmdParms); + this.streamCommand(CMD_PAUSE); }; this.play = function() { - this.streamCmdParms.command = CMD_PLAY; - this.streamCmdReq(this.streamCmdParms); + this.streamCommand(CMD_PLAY); }; this.eventHandler = function(event) { @@ -154,77 +154,142 @@ function MonitorStream(monitorData) { }; this.onclick = function(evt) { - var el = evt.currentTarget; - var id = el.getAttribute("data-monitor-id"); - var url = '?view=watch&mid='+id; - evt.preventDefault(); - window.location.assign(url); + console.log('onclick'); }; - this.setup_onclick = function() { - var el = document.getElementById('imageFeed'+this.id); - if ( el ) el.addEventListener('click', this.onclick, false); + this.setup_onclick = function(func) { + this.onclick = func; + const el = this.getElement(); + if (!el) return; + el.addEventListener('click', this.onclick, false); }; + this.disable_onclick = function() { - document.getElementById('imageFeed'+this.id).removeEventListener('click', this.onclick ); + const el = this.getElement(); + if (!el) return; + el.removeEventListener('click', this.onclick); + }; + + this.onpause = function() { + console.log('onpause'); + }; + this.setup_onpause = function(func) { + this.onpause = func; + }; + this.onplay = null; + this.setup_onplay = function(func) { + this.onplay = func; }; this.setStateClass = function(jobj, stateClass) { - if ( !jobj ) { + if (!jobj) { + console.log("No obj in setStateClass"); return; } - if ( !jobj.hasClass( stateClass ) ) { - if ( stateClass != 'alarm' ) jobj.removeClass('alarm'); - if ( stateClass != 'alert' ) jobj.removeClass('alert'); - if ( stateClass != 'idle' ) jobj.removeClass('idle'); + if (!jobj.hasClass(stateClass)) { + if (stateClass != 'alarm') jobj.removeClass('alarm'); + if (stateClass != 'alert') jobj.removeClass('alert'); + if (stateClass != 'idle') jobj.removeClass('idle'); jobj.addClass(stateClass); } }; + this.setAlarmState = function(alarmState) { + var stateClass = ''; + if (alarmState == STATE_ALARM) { + stateClass = 'alarm'; + } else if (alarmState == STATE_ALERT) { + stateClass = 'alert'; + } + + const stateValue = $j('#stateValue'+this.id); + if (stateValue.length) { + stateValue.text(stateStrings[alarmState]); + if (stateClass) { + stateValue.addClass(stateClass); + } else { + stateValue.removeClass(); + } + } else { + console.log("No statevalue"); + } + //const monitorState = $j('#monitorState'+this.id); + //if (monitorState.length) this.setStateClass(monitorState, stateClass); + + const isAlarmed = ( alarmState == STATE_ALARM || alarmState == STATE_ALERT ); + const wasAlarmed = ( this.lastAlarmState == STATE_ALARM || this.lastAlarmState == STATE_ALERT ); + + const newAlarm = ( isAlarmed && !wasAlarmed ); + const oldAlarm = ( !isAlarmed && wasAlarmed ); + + if (newAlarm) { + if (SOUND_ON_ALARM) { + // Enable the alarm sound + if (!msieVer) { + $j('#alarmSound').removeClass('hidden'); + } else { + $j('#MediaPlayer').trigger('play'); + } + } + if (POPUP_ON_ALARM) { + window.focus(); + } + if (this.onalarm) { + this.onalarm(); + } + } + if (oldAlarm) { // done with an event do a refresh + if (SOUND_ON_ALARM) { + // Disable alarm sound + if (!msieVer) { + $j('#alarmSound').addClass('hidden'); + } else { + $j('#MediaPlayer').trigger('pause'); + } + } + if (this.onalarm) { + this.onalarm(); + } + } + this.lastAlarmState = alarmState; + }; // end function setAlarmState( currentAlarmState ) + + this.onalarm = null; + this.setup_onalarm = function(func) { + this.onalarm = func; + }; + this.onFailure = function(jqxhr, textStatus, error) { + // Assuming temporary problem, retry in a bit. setTimeout(this.streamCmdQuery.bind(this), 1000*statusRefreshTimeout); logAjaxFail(jqxhr, textStatus, error); }; this.getStreamCmdResponse = function(respObj, respText) { - var stream = $j('#liveStream'+this.id)[0]; - + var stream = this.getElement(); if (!stream) { - console.log('No live stream'); return; } //watchdogOk('stream'); - if (streamCmdTimer) { - streamCmdTimer = clearTimeout(streamCmdTimer); + if (this.streamCmdTimer) { + this.streamCmdTimer = clearTimeout(this.streamCmdTimer); } - if ( respObj.result == 'Ok' ) { - if ( respObj.status ) { - this.status = respObj.status; - this.alarmState = this.status.state; - - var stateClass = ''; - if ( this.alarmState == STATE_ALARM ) { - stateClass = 'alarm'; - } else if ( this.alarmState == STATE_ALERT ) { - stateClass = 'alert'; - } else { - stateClass = 'idle'; - } + if (respObj.result == 'Ok') { + if (respObj.status) { + const streamStatus = this.status = respObj.status; if ( ( (typeof COMPACT_MONTAGE === 'undefined') || !COMPACT_MONTAGE) && (this.type != 'WebSite') ) { - const viewingFPSValue = $j('#vewingFPSValue'+this.id); + const viewingFPSValue = $j('#viewingFPSValue'+this.id); const captureFPSValue = $j('#captureFPSValue'+this.id); const analysisFPSValue = $j('#analysisFPSValue'+this.id); - const stateValue = $j('#stateValue'+this.id); - const monitorState = $j('#monitorState'+this.id); if (viewingFPSValue.length && (viewingFPSValue.text != this.status.fps)) { viewingFPSValue.text(this.status.fps); @@ -236,40 +301,104 @@ function MonitorStream(monitorData) { captureFPSValue.text(this.status.capturefps); } - if (stateValue.length) stateValue.text(stateStrings[this.alarmState]); - if (monitorState.length) this.setStateClass(monitorState, stateClass); - } - - this.setStateClass($j('#monitor'+this.id), stateClass); - - /*Stream could be an applet so can't use moo tools*/ - //stream.parentNode().className = stateClass; - - var isAlarmed = ( this.alarmState == STATE_ALARM || this.alarmState == STATE_ALERT ); - var wasAlarmed = ( this.lastAlarmState == STATE_ALARM || this.lastAlarmState == STATE_ALERT ); - - var newAlarm = ( isAlarmed && !wasAlarmed ); - var oldAlarm = ( !isAlarmed && wasAlarmed ); - - if (newAlarm) { - if (false && SOUND_ON_ALARM) { - // Enable the alarm sound - $j('#alarmSound').removeClass('hidden'); + const levelValue = $j('#levelValue'); + if (levelValue.length) { + levelValue.text(this.status.level); + var newClass = 'ok'; + if (this.status.level > 95) { + newClass = 'alarm'; + } else if (this.status.level > 80) { + newClass = 'alert'; + } + levelValue.removeClass(); + levelValue.addClass(newClass); } - if ((typeof POPUP_ON_ALARM !== 'undefined') && POPUP_ON_ALARM) { - windowToFront(); + + const delayString = secsToTime(this.status.delay); + + if (this.status.paused == true) { + $j('#modeValue'+this.id).text('Paused'); + $j('#rate'+this.id).addClass('hidden'); + $j('#delayValue'+this.id).text(delayString); + $j('#delay'+this.id).removeClass('hidden'); + $j('#level'+this.id).removeClass('hidden'); + this.onpause(); + } else if (this.status.delayed == true) { + $j('#modeValue'+this.id).text('Replay'); + $j('#rateValue'+this.id).text(this.status.rate); + $j('#rate'+this.id).removeClass('hidden'); + $j('#delayValue'+this.id).text(delayString); + $j('#delay'+this.id).removeClass('hidden'); + $j('#level'+this.id).removeClass('hidden'); + if (this.status.rate == 1) { + if (this.onplay) this.onplay(); + } else if (this.status.rate > 0) { + if (this.status.rate < 1) { + streamCmdSlowFwd(false); + } else { + streamCmdFastFwd(false); + } + } else { + if (this.status.rate > -1) { + streamCmdSlowRev(false); + } else { + streamCmdFastRev(false); + } + } // rate + } else { + $j('#modeValue'+this.id).text('Live'); + $j('#rate'+this.id).addClass('hidden'); + $j('#delay'+this.id).addClass('hidden'); + $j('#level'+this.id).addClass('hidden'); + if (this.onplay) this.onplay(); + } // end if paused or delayed + + $j('#zoomValue'+this.id).text(this.status.zoom); + if ('zoomOutBtn' in this.buttons) { + if (this.status.zoom == '1.0') { + setButtonState('zoomOutBtn', 'unavail'); + } else { + setButtonState('zoomOutBtn', 'inactive'); + } } - } - if (false && SOUND_ON_ALARM) { - if ( oldAlarm ) { - // Disable alarm sound - $j('#alarmSound').addClass('hidden'); + } // end if compact montage + + this.setAlarmState(this.status.state); + + if (canEdit.Monitors) { + if (streamStatus.enabled) { + if ('enableAlarmButton' in this.buttons) { + this.buttons.enableAlarmButton.addClass('disabled'); + this.buttons.enableAlarmButton.prop('title', disableAlarmsStr); + } + if ('forceAlarmButton' in this.buttons) { + if (streamStatus.forced) { + this.buttons.forceAlarmButton.addClass('disabled'); + this.buttons.forceAlarmButton.prop('title', cancelForcedAlarmStr); + } else { + this.buttons.forceAlarmButton.removeClass('disabled'); + this.buttons.forceAlarmButton.prop('title', forceAlarmStr); + } + this.buttons.forceAlarmButton.prop('disabled', false); + } + } else { + if ('enableAlarmButton' in this.buttons) { + this.buttons.enableAlarmButton.removeClass('disabled'); + this.buttons.enableAlarmButton.prop('title', enableAlarmsStr); + } + if ('forceAlarmButton' in this.buttons) { + this.buttons.forceAlarmButton.prop('disabled', true); + } } - } + if ('enableAlarmButton' in this.buttons) { + this.buttons.enableAlarmButton.prop('disabled', false); + } + } // end if canEdit.Monitors + if (this.status.auth) { if (this.status.auth != this.auth_hash) { // Try to reload the image stream. - if (stream) { + if (stream && stream.src) { const oldsrc = stream.src; stream.src = ''; stream.src = oldsrc.replace(/auth=\w+/i, 'auth='+this.status.auth); @@ -283,23 +412,25 @@ function MonitorStream(monitorData) { console.error(respObj.message); // Try to reload the image stream. if (stream) { - if ( stream.src ) { + if (stream.src) { console.log('Reloading stream: ' + stream.src); src = stream.src.replace(/rand=\d+/i, 'rand='+Math.floor((Math.random() * 1000000) )); - if ( src != stream.src ) { + // Maybe navbar updated auth FIXME + if (src != stream.src) { stream.src = src; } else { console.log("Failed to update rand on stream src"); } - } else { } } else { console.log('No stream to reload?'); } } // end if Ok or not + }; - this.lastAlarmState = this.alarmState; - setTimeout(this.streamCmdQuery.bind(this), statusRefreshTimeout); + this.statusQuery = function() { + this.streamCmdQuery(CMD_QUERY); + setTimeout(this.statusQuery.bind(this), statusRefreshTimeout); }; this.streamCmdQuery = function(resent) { @@ -310,15 +441,44 @@ function MonitorStream(monitorData) { } }; - if ( this.type != 'WebSite' ) { + this.streamCommand = function(command) { + if (typeof(command) == 'object') { + for (const key in command) this.streamCmdParms[key] = command[key]; + } else { + this.streamCmdParms.command = command; + } + this.streamCmdReq(this.streamCmdParms); + }; + + this.alarmCommand = function(command) { + if (this.ajaxQueue) { + this.ajaxQueue.abort(); + } + const alarmCmdParms = Object.assign({}, this.streamCmdParms); + alarmCmdParms.request = 'alarm'; + alarmCmdParms.command = command; + alarmCmdParms.id = this.id; + + this.ajaxQueue = jQuery.ajaxQueue({ + url: this.url, + data: alarmCmdParms, dataType: "json"}) + .done(this.getStreamCmdResponse.bind(this)) + .fail(this.onFailure.bind(this)); + }; + + if (this.type != 'WebSite') { + $j.ajaxSetup({timeout: AJAX_TIMEOUT}); + if (auth_hash) { + this.streamCmdParms.auth = auth_hash; + } else if ( auth_relay ) { + this.streamCmdParms.auth_relay = ''; + } + this.streamCmdReq = function(streamCmdParms) { - if ( auth_hash ) { - this.streamCmdParms.auth = auth_hash; - } else if ( auth_relay ) { - this.streamCmdParms.auth_relay = ''; + if (this.ajaxQueue) { + this.ajaxQueue.abort(); } - $j.ajaxSetup({timeout: AJAX_TIMEOUT}); - $j.getJSON(this.url, streamCmdParms) + this.ajaxQueue = jQuery.ajaxQueue({url: this.url, data: streamCmdParms, dataType: "json"}) .done(this.getStreamCmdResponse.bind(this)) .fail(this.onFailure.bind(this)); }; @@ -326,7 +486,7 @@ function MonitorStream(monitorData) { this.analyse_frames = true; this.show_analyse_frames = function(toggle) { this.analyse_frames = toggle; - this.streamCmdParms.command = this.analyse_frames?CMD_ANALYZE_ON:CMD_ANALYZE_OFF; + this.streamCmdParms.command = this.analyse_frames ? CMD_ANALYZE_ON : CMD_ANALYZE_OFF; this.streamCmdReq(this.streamCmdParms); }; } // end function MonitorStream From 2da9c20c08be3103c66bc05775b95430b301c3a7 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 20 Jan 2022 11:49:59 -0500 Subject: [PATCH 18/48] Add ajaxQueue --- web/js/ajaxQueue.js | 48 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 web/js/ajaxQueue.js diff --git a/web/js/ajaxQueue.js b/web/js/ajaxQueue.js new file mode 100644 index 000000000..c706fb978 --- /dev/null +++ b/web/js/ajaxQueue.js @@ -0,0 +1,48 @@ +// See https://github.com/gnarf/jquery-ajaxQueue for license and copyright + +(function($) { + +// jQuery on an empty object, we are going to use this as our Queue +var ajaxQueue = $({}); + +$.ajaxQueue = function( ajaxOpts ) { + var jqXHR, + dfd = $.Deferred(), + promise = dfd.promise(); + + // run the actual query + function doRequest( next ) { + jqXHR = $.ajax( ajaxOpts ); + jqXHR.done( dfd.resolve ) + .fail( dfd.reject ) + .then( next, next ); + } + + // queue our ajax request + ajaxQueue.queue( doRequest ); + + // add the abort method + promise.abort = function( statusText ) { + + // proxy abort to the jqXHR if it is active + if ( jqXHR ) { + return jqXHR.abort( statusText ); + } + + // if there wasn't already a jqXHR we need to remove from queue + var queue = ajaxQueue.queue(), + index = $.inArray( doRequest, queue ); + + if ( index > -1 ) { + queue.splice( index, 1 ); + } + + // and then reject the deferred + dfd.rejectWith( ajaxOpts.context || ajaxOpts, [ promise, statusText, "" ] ); + return promise; + }; + + return promise; +}; + +})(jQuery); From 97f2c0a02b8f5a0b001883c2d7f9cf59ed0787ac Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 20 Jan 2022 11:50:25 -0500 Subject: [PATCH 19/48] Load ajaxQueue --- web/skins/classic/includes/functions.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/skins/classic/includes/functions.php b/web/skins/classic/includes/functions.php index c593601aa..beb6590e0 100644 --- a/web/skins/classic/includes/functions.php +++ b/web/skins/classic/includes/functions.php @@ -916,6 +916,7 @@ function xhtmlFooter() { ?> + /js/moment.min.js"> - + From 57bb91e10550a9fae50e983ed2b9ffaccb25320d Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 20 Jan 2022 11:53:05 -0500 Subject: [PATCH 24/48] add connKey to monitorData --- web/skins/classic/views/js/watch.js.php | 1 + 1 file changed, 1 insertion(+) diff --git a/web/skins/classic/views/js/watch.js.php b/web/skins/classic/views/js/watch.js.php index 2f383ab24..cef3cdd79 100644 --- a/web/skins/classic/views/js/watch.js.php +++ b/web/skins/classic/views/js/watch.js.php @@ -64,6 +64,7 @@ foreach ($monitors as $m) { ?> monitorData[monitorData.length] = { 'id': Id() ?>, + 'connKey': connKey() ?>, 'width': ViewWidth() ?>, 'height':ViewHeight() ?>, 'janusEnabled':JanusEnabled() ?>, From 91d6ff20298a66852e48ec974f43e9b5ee1cb059 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 20 Jan 2022 11:53:35 -0500 Subject: [PATCH 25/48] Use Monitor->getStatusHTML to generate status html --- web/skins/classic/views/zone.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/skins/classic/views/zone.php b/web/skins/classic/views/zone.php index 5031c316f..4c2cc2d30 100644 --- a/web/skins/classic/views/zone.php +++ b/web/skins/classic/views/zone.php @@ -164,10 +164,10 @@ if ( count($other_zones) ) { Sorry, your browser does not support inline SVG - -
-  -  fps -
+ getMonitorStateHTML(); +?> +
From 4eb4e0eb185d2996e207f1039674e58bf4601ef8 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 20 Jan 2022 12:02:17 -0500 Subject: [PATCH 27/48] ignore ajaxQueue.js --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 2e2fdd39a..f5529e8ee 100644 --- a/.eslintignore +++ b/.eslintignore @@ -14,6 +14,7 @@ web/skins/classic/js/moment.js web/skins/classic/js/video.js web/tools/mootools web/js/janus.js +web/js/ajaxQueue.js # Cannot be parsed as JS web/skins/classic/includes/export_functions.php From 52e48c02b61bcb9fb2fedc79b5d8b546d74d2b2c Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 21 Jan 2022 22:23:41 -0600 Subject: [PATCH 28/48] Add janus_path and janus_secret, allowing for more secure and flexible Janus installs --- .../lib/ZoneMinder/ConfigData.pm.in | 22 ++++++++++++++++ src/zm_monitor.cpp | 26 +++++++++++++++---- web/includes/functions.php | 2 +- web/js/MonitorStream.js | 4 ++- web/skins/classic/views/cycle.php | 1 + web/skins/classic/views/js/cycle.js | 9 ++++++- web/skins/classic/views/montage.php | 1 + web/skins/classic/views/watch.php | 1 + 8 files changed, 58 insertions(+), 8 deletions(-) diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in index 48d494614..9b8a4e61e 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in @@ -370,6 +370,28 @@ our @options = ( type => $types{boolean}, category => 'system', }, + { + name => 'ZM_JANUS_SECRET', + default => '', + description => 'Password for Janus streaming administration.', + help => q`This value should be set to a secure password, + and match the admin_key value in janus.plugin.streaming.config. + `, + type => $types{string}, + category => 'system', + }, + { + name => 'ZM_JANUS_PATH', + default => '', + description => 'URL for Janus HTTP/S port', + help => q`Janus requires HTTP/S communication to administer + and initiate h.264 streams. If left blank, this will default to + the ZM hostname, port 8088/janus. This setting is particularly + useful for putting janus behind a reverse proxy. + `, + type => $types{string}, + category => 'system', + }, { name => 'ZM_ENABLE_CSRF_MAGIC', default => 'yes', diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 2179197e8..9a1b747b6 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -3383,7 +3383,12 @@ size_t Monitor::WriteCallback(void *contents, size_t size, size_t nmemb, void *u int Monitor::add_to_janus() { //TODO clean this up, add error checking, etc std::string response; - std::string endpoint = "127.0.0.1:8088/janus/"; + std::string endpoint; + if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) { + endpoint = config.janus_path; + } else { + endpoint = "127.0.0.1:8088/janus/"; + } std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; std::string rtsp_username; std::string rtsp_password; @@ -3425,6 +3430,7 @@ int Monitor::add_to_janus() { if (pos == std::string::npos) return -1; janus_id = response.substr(pos + 6, 16); response = ""; + endpoint += "/"; endpoint += janus_id; postData = "{\"janus\" : \"attach\", \"plugin\" : \"janus.plugin.streaming\", \"transaction\" : \"randomString\"}"; curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); @@ -3445,8 +3451,10 @@ int Monitor::add_to_janus() { //Assemble our actual request postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; - postData += "\"request\" : \"create\", \"admin_key\" : \"supersecret\", \"type\" : \"rtsp\", "; - postData += "\"url\" : \""; + postData += "\"request\" : \"create\", \"admin_key\" : \""; + postData += config.janus_secret; + postData += "\", \"type\" : \"rtsp\", "; + postData += "\"url\" : \""; postData += rtsp_path; postData += "\", \"rtsp_user\" : \""; postData += rtsp_username; @@ -3474,7 +3482,12 @@ int Monitor::add_to_janus() { int Monitor::remove_from_janus() { //TODO clean this up, add error checking, etc std::string response; - std::string endpoint = "127.0.0.1:8088/janus/"; + std::string endpoint; + if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) { + endpoint = config.janus_path; + } else { + endpoint = "127.0.0.1:8088/janus/"; + } std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; std::size_t pos; CURLcode res; @@ -3495,6 +3508,7 @@ int Monitor::remove_from_janus() { if (pos == std::string::npos) return -1; std::string janus_id = response.substr(pos + 6, 16); response = ""; + endpoint += "/"; endpoint += janus_id; postData = "{\"janus\" : \"attach\", \"plugin\" : \"janus.plugin.streaming\", \"transaction\" : \"randomString\"}"; curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); @@ -3512,7 +3526,9 @@ int Monitor::remove_from_janus() { //Assemble our actual request postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; - postData += "\"request\" : \"destroy\", \"admin_key\" : \"supersecret\", \"id\" : "; + postData += "\"request\" : \"destroy\", \"admin_key\" : \""; + postData += config.janus_secret; + postData += "\", \"id\" : "; postData += std::to_string(id); postData += "}}"; diff --git a/web/includes/functions.php b/web/includes/functions.php index f9b8abf72..e346ceb34 100644 --- a/web/includes/functions.php +++ b/web/includes/functions.php @@ -2095,7 +2095,7 @@ function getStreamHTML($monitor, $options = array()) { ) ); return getVideoStreamHTML( 'liveStream'.$monitor->Id(), $streamSrc, $options['width'], $options['height'], ZM_MPEG_LIVE_FORMAT, $monitor->Name() ); } else if ( $monitor->JanusEnabled() ) { - return ''; + return ''; } else if ( $options['mode'] == 'stream' and canStream() ) { $options['mode'] = 'jpeg'; $streamSrc = $monitor->getStreamSrc($options); diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index 17f43c4b9..373902e1f 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -92,7 +92,9 @@ function MonitorStream(monitorData) { if (this.janusEnabled) { var id = parseInt(this.id); var server; - if (window.location.protocol=='https:') { + if (ZM_JANUS_PATH) { + server = ZM_JANUS_PATH; + } else if (window.location.protocol=='https:') { // Assume reverse proxy setup for now server = "https://" + window.location.hostname + "/janus"; } else { diff --git a/web/skins/classic/views/cycle.php b/web/skins/classic/views/cycle.php index ee9e03d4f..50bb42045 100644 --- a/web/skins/classic/views/cycle.php +++ b/web/skins/classic/views/cycle.php @@ -192,6 +192,7 @@ xhtmlHeaders(__FILE__, translate('CycleWatch')); + diff --git a/web/skins/classic/views/js/cycle.js b/web/skins/classic/views/js/cycle.js index 670671faa..9e128d0ab 100644 --- a/web/skins/classic/views/js/cycle.js +++ b/web/skins/classic/views/js/cycle.js @@ -51,7 +51,14 @@ function initCycle() { if ( scale == '0' || scale == 'auto' ) changeScale(); if (monitorData[monIdx].janusEnabled) { - server = "http://" + window.location.hostname + ":8088/janus"; + if (ZM_JANUS_PATH) { + server = ZM_JANUS_PATH; + } else if (window.location.protocol=='https:') { + // Assume reverse proxy setup for now + server = "https://" + window.location.hostname + "/janus"; + } else { + server = "http://" + window.location.hostname + ":8088/janus"; + } opaqueId = "streamingtest-"+Janus.randomString(12); Janus.init({debug: "all", callback: function() { janus = new Janus({ diff --git a/web/skins/classic/views/montage.php b/web/skins/classic/views/montage.php index a96e8a88e..c59ce9f66 100644 --- a/web/skins/classic/views/montage.php +++ b/web/skins/classic/views/montage.php @@ -318,6 +318,7 @@ foreach (array_reverse($zones) as $zone) { + diff --git a/web/skins/classic/views/watch.php b/web/skins/classic/views/watch.php index 340ee8e33..46ee11f90 100644 --- a/web/skins/classic/views/watch.php +++ b/web/skins/classic/views/watch.php @@ -396,6 +396,7 @@ if ( ZM_WEB_SOUND_ON_ALARM ) { ?> + From 2e9bda1af190a3f18ae06eb7bbe10b8f8f08f49c Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 21 Jan 2022 23:21:41 -0600 Subject: [PATCH 29/48] Add firefox specific workaround for Janus streaming --- web/js/MonitorStream.js | 3 +++ web/skins/classic/views/js/cycle.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index 373902e1f..b5f9d0b6a 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -521,6 +521,9 @@ async function attachVideo(id) { if (jsep !== undefined && jsep !== null) { Janus.debug("Handling SDP as well..."); Janus.debug(jsep); + if ((navigator.userAgent.toLowerCase().indexOf('firefox') > -1) && (jsep["sdp"].includes("420029"))) { //because firefox devs are stubborn + jsep["sdp"] = jsep["sdp"].replace("420029", "42e01f"); + } // Offer from the plugin, let's answer streaming[id].createAnswer({ jsep: jsep, diff --git a/web/skins/classic/views/js/cycle.js b/web/skins/classic/views/js/cycle.js index 9e128d0ab..7e7bd0147 100644 --- a/web/skins/classic/views/js/cycle.js +++ b/web/skins/classic/views/js/cycle.js @@ -91,6 +91,9 @@ function initCycle() { if (jsep !== undefined && jsep !== null) { Janus.debug("Handling SDP as well..."); Janus.debug(jsep); + if ((navigator.userAgent.toLowerCase().indexOf('firefox') > -1) && (jsep["sdp"].includes("420029"))) { //because firefox devs are stubborn + jsep["sdp"] = jsep["sdp"].replace("420029", "42e01f"); + } // Offer from the plugin, let's answer streaming2.createAnswer({ jsep: jsep, From 45db266ede1036b79823298cc7bd6239954e885d Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 22 Jan 2022 12:07:12 -0500 Subject: [PATCH 30/48] Put full config available to javascript --- web/skins/classic/js/skin.js.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/skins/classic/js/skin.js.php b/web/skins/classic/js/skin.js.php index 2b7980c3a..6cde755b6 100644 --- a/web/skins/classic/js/skin.js.php +++ b/web/skins/classic/js/skin.js.php @@ -105,3 +105,10 @@ stateStrings[STATE_PREALARM] = ""; stateStrings[STATE_ALARM] = ""; stateStrings[STATE_ALERT] = ""; stateStrings[STATE_TAPE] = ""; + +$c) { + echo $name . ' = \''.$c['Value'].'\''.PHP_EOL; +} +?> From 35efb111ac2f264387d999cbac7adb98451787d1 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 22 Jan 2022 12:30:59 -0500 Subject: [PATCH 31/48] Add Private to Config --- db/zm_create.sql.in | 1 + db/zm_update-1.37.10.sql | 20 +++++++++++++++++++ .../ZoneMinder/lib/ZoneMinder/Config.pm.in | 5 +++-- .../lib/ZoneMinder/ConfigData.pm.in | 3 +++ 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 db/zm_update-1.37.10.sql diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index db72cb58d..7dc77cc47 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -39,6 +39,7 @@ CREATE TABLE `Config` ( `Help` text, `Category` varchar(32) NOT NULL default '', `Readonly` tinyint(3) unsigned NOT NULL default '0', + `Private` BOOLEAN NOT NULL DEFAULT FALSE, `Requires` text, PRIMARY KEY (`Name`) ) ENGINE=@ZM_MYSQL_ENGINE@; diff --git a/db/zm_update-1.37.10.sql b/db/zm_update-1.37.10.sql new file mode 100644 index 000000000..30c2d8766 --- /dev/null +++ b/db/zm_update-1.37.10.sql @@ -0,0 +1,20 @@ +-- +-- Update Config table to have Private +-- + +SELECT 'Checking for Private in Config'; +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Config' + AND table_schema = DATABASE() + AND column_name = 'Private' + ) > 0, +"SELECT 'Column Private already exists in Config'", +"ALTER TABLE `Config` ADD COLUMN `Private` BOOLEAN NOT NULL DEFAULT FALSE AFTER `Readonly`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + + diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Config.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/Config.pm.in index 21f9a2d02..849ec5450 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Config.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/Config.pm.in @@ -156,7 +156,7 @@ sub loadConfigFromDB { print("Error: unable to load options from database: $DBI::errstr\n"); return(0); } - my $sql = "select * from Config"; + my $sql = 'SELECT * FROM Config'; my $sth = $dbh->prepare_cached( $sql ) or croak( "Can't prepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute() @@ -203,7 +203,7 @@ sub saveConfigToDB { my $res = $dbh->do( $sql ) or croak( "Can't do '$sql': ".$dbh->errstr() ); - $sql = "replace into Config set Id = ?, Name = ?, Value = ?, Type = ?, DefaultValue = ?, Hint = ?, Pattern = ?, Format = ?, Prompt = ?, Help = ?, Category = ?, Readonly = ?, Requires = ?"; + $sql = "replace into Config set Id = ?, Name = ?, Value = ?, Type = ?, DefaultValue = ?, Hint = ?, Pattern = ?, Format = ?, Prompt = ?, Help = ?, Category = ?, Readonly = ?, Private = ?, Requires = ?"; my $sth = $dbh->prepare_cached( $sql ) or croak( "Can't prepare '$sql': ".$dbh->errstr() ); foreach my $option ( @options ) { @@ -240,6 +240,7 @@ sub saveConfigToDB { $option->{help}, $option->{category}, $option->{readonly} ? 1 : 0, + $option->{private} ? 1 : 0, $option->{db_requires} ) or croak("Can't execute when updating config entry $$option{name}: ".$sth->errstr() ); } # end foreach option diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in index 48d494614..d9d3c5306 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in @@ -304,6 +304,7 @@ our @options = ( { name=>'ZM_AUTH_RELAY', value=>'hashed' } ], type => $types{string}, + private => 1, category => 'system', }, { @@ -462,6 +463,7 @@ our @options = ( {name=>'ZM_OPT_USE_GOOG_RECAPTCHA', value=>'yes'} ], type => $types{string}, + private => 1, category => 'system', }, { @@ -477,6 +479,7 @@ our @options = ( {name=>'ZM_OPT_USE_GOOG_RECAPTCHA', value=>'yes'} ], type => $types{string}, + private => 1, category => 'system', }, { From ce2d605b3d0eaa498ddc51a2b09e38a07623f57e Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 22 Jan 2022 12:31:15 -0500 Subject: [PATCH 32/48] load Private as well as Name,Value from Config --- web/includes/config.php.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/includes/config.php.in b/web/includes/config.php.in index b30aebdea..c463a14ae 100644 --- a/web/includes/config.php.in +++ b/web/includes/config.php.in @@ -165,7 +165,7 @@ function loadConfig( $defineConsts=true ) { $config = array(); - $result = $dbConn->query('SELECT Name,Value FROM Config'); + $result = $dbConn->query('SELECT Name,Value,Private FROM Config'); if ( !$result ) echo mysql_error(); while( $row = dbFetchNext($result) ) { From 3a0b88c013709a1336b8e86c0d3ab83f06ed6a5f Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 22 Jan 2022 12:31:36 -0500 Subject: [PATCH 33/48] Don't make private config entries available to js land. --- web/skins/classic/js/skin.js.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/skins/classic/js/skin.js.php b/web/skins/classic/js/skin.js.php index 6cde755b6..336c0a352 100644 --- a/web/skins/classic/js/skin.js.php +++ b/web/skins/classic/js/skin.js.php @@ -109,6 +109,7 @@ stateStrings[STATE_TAPE] = ""; $c) { - echo $name . ' = \''.$c['Value'].'\''.PHP_EOL; + if (!$c['Private']) + echo $name . ' = \''.$c['Value'].'\''.PHP_EOL; } ?> From 47aa1d1913319d47a540efe98147077b7717612e Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 22 Jan 2022 12:31:56 -0500 Subject: [PATCH 34/48] Bump version to 1.37.10 --- distros/redhat/zoneminder.spec | 2 +- version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/distros/redhat/zoneminder.spec b/distros/redhat/zoneminder.spec index 16b8efb7e..b95d80626 100644 --- a/distros/redhat/zoneminder.spec +++ b/distros/redhat/zoneminder.spec @@ -36,7 +36,7 @@ %global _hardened_build 1 Name: zoneminder -Version: 1.37.9 +Version: 1.37.10 Release: 1%{?dist} Summary: A camera monitoring and analysis tool Group: System Environment/Daemons diff --git a/version b/version index 938adc6f6..a9009bf2a 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.37.9 +1.37.10 From 90f75dae93214bf0f9300183cfb855f87d662445 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 22 Jan 2022 12:33:48 -0500 Subject: [PATCH 35/48] Make config entries const --- web/skins/classic/js/skin.js.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/skins/classic/js/skin.js.php b/web/skins/classic/js/skin.js.php index 336c0a352..2d443fbe1 100644 --- a/web/skins/classic/js/skin.js.php +++ b/web/skins/classic/js/skin.js.php @@ -110,6 +110,6 @@ stateStrings[STATE_TAPE] = ""; global $config; foreach ($config as $name=>$c) { if (!$c['Private']) - echo $name . ' = \''.$c['Value'].'\''.PHP_EOL; + echo 'const '. $name . ' = \''.$c['Value'].'\''.PHP_EOL; } ?> From 9443aaa2d866ee7d60fa3e4f9c083bdc2df0359a Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 22 Jan 2022 13:33:39 -0500 Subject: [PATCH 36/48] Do state changes when in RECORD mode as well as it doesn't do motion detection Fixes #3411 --- src/zm_monitor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 2179197e8..9952f727f 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -2120,7 +2120,7 @@ bool Monitor::Analyse() { // snap->score -1 means didn't do motion detection so don't do state transition // In Nodect, we may still have a triggered event, so need this code to run to end the event. - } else if (!score and ((snap->score == 0) or (function == NODECT))) { + } else if (!score and ((snap->score == 0) or (function == NODECT || function == RECORD))) { Debug(1, "!score %s", State_Strings[state].c_str()); alert_to_alarm_frame_count = alarm_frame_count; // load same value configured for alarm_frame_count From 07c5b23aa6668e33d8b21204953e408994d9a9db Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 22 Jan 2022 11:53:44 -0600 Subject: [PATCH 37/48] Adds Janus streaming checks to polling thread --- src/zm_monitor.cpp | 91 ++++++++++++++++++++++++++++++++++++++++------ src/zm_monitor.h | 1 + 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 9a1b747b6..cc426586c 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #if ZM_MEM_MAPPED #include @@ -1123,9 +1124,7 @@ bool Monitor::connect() { #if HAVE_LIBCURL //janus setup. Depends on libcurl. if (janus_enabled && (path.find("rtsp://") != std::string::npos)) { if (add_to_janus() != 0) { - if (add_to_janus() != 0) { //The initial attempt may fail. This is a temporary workaround. - Warning("Failed to add monitor stream to Janus!"); - } + Warning("Failed to add monitor stream to Janus!"); //The first attempt may fail. Will be reattempted in the Poller thread } } #else @@ -1797,6 +1796,8 @@ void Monitor::UpdateFPS() { //Thread where ONVIF polling, and other similar status polling can happen. //Since these can be blocking, run here to avoid intefering with other processing bool Monitor::Poll() { + //We want to trigger every 5 seconds or so. so grab the time at the beginning of the loop, and sleep at the end. + std::chrono::system_clock::time_point loop_start_time = std::chrono::system_clock::now(); #ifdef WITH_GSOAP if (ONVIF_Healthy) { @@ -1838,10 +1839,14 @@ bool Monitor::Poll() { } } } - } else { - std::this_thread::sleep_for (std::chrono::seconds(1)); //thread sleep to avoid the busy loop. } #endif + if (janus_enabled) { + if (check_janus() != 1) { + add_to_janus(); + } + } + std::this_thread::sleep_until(loop_start_time + std::chrono::seconds(5)); return TRUE; } //end Poll @@ -3166,10 +3171,8 @@ int Monitor::PrimeCapture() { } } // end if rtsp_server -#ifdef WITH_GSOAP //For now, just don't run the thread if no ONVIF support. This may change if we add other long polling options. - //ONVIF Thread - - if (onvif_event_listener && ONVIF_Healthy) { + //Poller Thread + if (onvif_event_listener || janus_enabled) { if (!Poller) { Poller = zm::make_unique(this); @@ -3177,7 +3180,7 @@ int Monitor::PrimeCapture() { Poller->Start(); } } -#endif + if (decoding_enabled) { if (!decoder_it) decoder_it = packetqueue.get_video_it(false); if (!decoder) { @@ -3381,7 +3384,6 @@ size_t Monitor::WriteCallback(void *contents, size_t size, size_t nmemb, void *u } int Monitor::add_to_janus() { - //TODO clean this up, add error checking, etc std::string response; std::string endpoint; if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) { @@ -3479,8 +3481,73 @@ int Monitor::add_to_janus() { curl_easy_cleanup(curl); return 0; } + +int Monitor::check_janus() { + std::string response; + std::string endpoint; + if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) { + endpoint = config.janus_path; + } else { + endpoint = "127.0.0.1:8088/janus/"; + } + std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; + std::size_t pos; + CURLcode res; + + curl = curl_easy_init(); + if(!curl) return -1; + + + //Start Janus API init. Need to get a session_id and handle_id + curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) return -1; + + pos = response.find("\"id\": "); + if (pos == std::string::npos) return -1; + std::string janus_id = response.substr(pos + 6, 16); + response = ""; + endpoint += "/"; + endpoint += janus_id; + postData = "{\"janus\" : \"attach\", \"plugin\" : \"janus.plugin.streaming\", \"transaction\" : \"randomString\"}"; + curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) return -1; + + pos = response.find("\"id\": "); + if (pos == std::string::npos) return -1; + std::string handle_id = response.substr(pos + 6, 16); + endpoint += "/"; + endpoint += handle_id; + + //Assemble our actual request + postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; + postData += "\"request\" : \"info\", \"id\" : "; + postData += std::to_string(id); + postData += "}}"; + + curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) return -1; + + Debug(1, "Queried for stream status: %s", response.c_str()); + curl_easy_cleanup(curl); + if (response.find("No such mountpoint") == std::string::npos) { + return 1; + } else { + return 0; + } +} int Monitor::remove_from_janus() { - //TODO clean this up, add error checking, etc std::string response; std::string endpoint; if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) { diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 898349eb3..381855c97 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -521,6 +521,7 @@ public: bool OnvifEnabled() { return onvif_event_listener; } + int check_janus(); //returns 1 for healthy, 0 for success but missing stream, negative for error. #ifdef WITH_GSOAP bool OnvifHealthy() { return ONVIF_Healthy; From abbd27d1cba082ea9bb4ec16ebb75c528f8d7bb9 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 22 Jan 2022 13:31:47 -0600 Subject: [PATCH 38/48] Remove hard-coded config Vars --- web/skins/classic/views/cycle.php | 1 - web/skins/classic/views/montage.php | 1 - web/skins/classic/views/watch.php | 1 - 3 files changed, 3 deletions(-) diff --git a/web/skins/classic/views/cycle.php b/web/skins/classic/views/cycle.php index 50bb42045..ee9e03d4f 100644 --- a/web/skins/classic/views/cycle.php +++ b/web/skins/classic/views/cycle.php @@ -192,7 +192,6 @@ xhtmlHeaders(__FILE__, translate('CycleWatch')); - diff --git a/web/skins/classic/views/montage.php b/web/skins/classic/views/montage.php index c59ce9f66..a96e8a88e 100644 --- a/web/skins/classic/views/montage.php +++ b/web/skins/classic/views/montage.php @@ -318,7 +318,6 @@ foreach (array_reverse($zones) as $zone) { - diff --git a/web/skins/classic/views/watch.php b/web/skins/classic/views/watch.php index 46ee11f90..340ee8e33 100644 --- a/web/skins/classic/views/watch.php +++ b/web/skins/classic/views/watch.php @@ -396,7 +396,6 @@ if ( ZM_WEB_SOUND_ON_ALARM ) { ?> - From d11098793571b6c9b0dc17f5aead5c5d3ad32e2d Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 22 Jan 2022 13:37:44 -0600 Subject: [PATCH 39/48] Fix indentation for ESLint --- web/skins/classic/views/js/cycle.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/web/skins/classic/views/js/cycle.js b/web/skins/classic/views/js/cycle.js index 7e7bd0147..19f4497be 100644 --- a/web/skins/classic/views/js/cycle.js +++ b/web/skins/classic/views/js/cycle.js @@ -51,14 +51,14 @@ function initCycle() { if ( scale == '0' || scale == 'auto' ) changeScale(); if (monitorData[monIdx].janusEnabled) { - if (ZM_JANUS_PATH) { - server = ZM_JANUS_PATH; - } else if (window.location.protocol=='https:') { - // Assume reverse proxy setup for now - server = "https://" + window.location.hostname + "/janus"; - } else { - server = "http://" + window.location.hostname + ":8088/janus"; - } + if (ZM_JANUS_PATH) { + server = ZM_JANUS_PATH; + } else if (window.location.protocol=='https:') { + // Assume reverse proxy setup for now + server = "https://" + window.location.hostname + "/janus"; + } else { + server = "http://" + window.location.hostname + ":8088/janus"; + } opaqueId = "streamingtest-"+Janus.randomString(12); Janus.init({debug: "all", callback: function() { janus = new Janus({ From 5485c04bc6fdb0151b44a30ee7a2a95ca842189f Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 23 Jan 2022 01:07:48 -0600 Subject: [PATCH 40/48] Rework of the Janus polling loop --- src/zm_monitor.cpp | 200 +++++++++++++++++++++++---------------------- src/zm_monitor.h | 2 + 2 files changed, 104 insertions(+), 98 deletions(-) diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index cc426586c..25a8c9f73 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -1123,6 +1123,7 @@ bool Monitor::connect() { #if HAVE_LIBCURL //janus setup. Depends on libcurl. if (janus_enabled && (path.find("rtsp://") != std::string::npos)) { + get_janus_session(); if (add_to_janus() != 0) { Warning("Failed to add monitor stream to Janus!"); //The first attempt may fail. Will be reattempted in the Poller thread } @@ -1842,7 +1843,10 @@ bool Monitor::Poll() { } #endif if (janus_enabled) { - if (check_janus() != 1) { + if (janus_session.empty()) { + get_janus_session(); + } + if (check_janus() == 0) { add_to_janus(); } } @@ -3395,7 +3399,6 @@ int Monitor::add_to_janus() { std::string rtsp_username; std::string rtsp_password; std::string rtsp_path = "rtsp://"; - std::string janus_id; std::size_t pos; std::size_t pos2; CURLcode res; @@ -3416,40 +3419,8 @@ int Monitor::add_to_janus() { rtsp_password = path.substr(pos+1, pos2 - pos - 1); rtsp_path += path.substr(pos2 + 1); - //Start Janus API init. Need to get a session_id and handle_id - curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); - res = curl_easy_perform(curl); - if (res != CURLE_OK) { - Error("Failed to curl_easy_perform getting session/handle id"); - curl_easy_cleanup(curl); - return -1; - } - - pos = response.find("\"id\": "); - if (pos == std::string::npos) return -1; - janus_id = response.substr(pos + 6, 16); - response = ""; endpoint += "/"; - endpoint += janus_id; - postData = "{\"janus\" : \"attach\", \"plugin\" : \"janus.plugin.streaming\", \"transaction\" : \"randomString\"}"; - curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); - res = curl_easy_perform(curl); - if (res != CURLE_OK) { - Error("Failed to curl_easy_perform attaching"); - curl_easy_cleanup(curl); - return -1; - } - pos = response.find("\"id\": "); - if (pos == std::string::npos) return -1; - std::string handle_id = response.substr(pos + 6, 16); //TODO: This is an assumption that the string is always 16 - endpoint += "/"; - endpoint += handle_id; + endpoint += janus_session; //Assemble our actual request postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; @@ -3477,6 +3448,13 @@ int Monitor::add_to_janus() { curl_easy_cleanup(curl); return -1; } + if ((response.find("error") != std::string::npos) && ((response.find("No such session") != std::string::npos) || (response.find("No such handle") != std::string::npos))) { + janus_session = ""; + curl_easy_cleanup(curl); + return -2; + } + //scan for missing session or handle id "No such session" "no such handle" + Debug(1,"Added stream to Janus: %s", response.c_str()); curl_easy_cleanup(curl); return 0; @@ -3490,41 +3468,15 @@ int Monitor::check_janus() { } else { endpoint = "127.0.0.1:8088/janus/"; } - std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; - std::size_t pos; + std::string postData; + //std::size_t pos; CURLcode res; curl = curl_easy_init(); if(!curl) return -1; - - //Start Janus API init. Need to get a session_id and handle_id - curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); - res = curl_easy_perform(curl); - if (res != CURLE_OK) return -1; - - pos = response.find("\"id\": "); - if (pos == std::string::npos) return -1; - std::string janus_id = response.substr(pos + 6, 16); - response = ""; endpoint += "/"; - endpoint += janus_id; - postData = "{\"janus\" : \"attach\", \"plugin\" : \"janus.plugin.streaming\", \"transaction\" : \"randomString\"}"; - curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); - res = curl_easy_perform(curl); - if (res != CURLE_OK) return -1; - - pos = response.find("\"id\": "); - if (pos == std::string::npos) return -1; - std::string handle_id = response.substr(pos + 6, 16); - endpoint += "/"; - endpoint += handle_id; + endpoint += janus_session; //Assemble our actual request postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; @@ -3537,16 +3489,28 @@ int Monitor::check_janus() { curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); res = curl_easy_perform(curl); - if (res != CURLE_OK) return -1; + if (res != CURLE_OK) { //may mean an error code thrown by Janus, because of a bad session + Warning("Attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); + curl_easy_cleanup(curl); + janus_session = ""; + return -1; + } - Debug(1, "Queried for stream status: %s", response.c_str()); curl_easy_cleanup(curl); - if (response.find("No such mountpoint") == std::string::npos) { - return 1; - } else { + Debug(1, "Queried for stream status: %s", response.c_str()); + if ((response.find("error") != std::string::npos) && ((response.find("No such session") != std::string::npos) || (response.find("No such handle") != std::string::npos))) { + Warning("Janus Session timed out"); + janus_session = ""; + return -2; + } else if (response.find("No such mountpoint") != std::string::npos) { + Warning("Mountpoint Missing"); return 0; + } else { + return 1; } } + + int Monitor::remove_from_janus() { std::string response; std::string endpoint; @@ -3556,40 +3520,14 @@ int Monitor::remove_from_janus() { endpoint = "127.0.0.1:8088/janus/"; } std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; - std::size_t pos; + //std::size_t pos; CURLcode res; curl = curl_easy_init(); if(!curl) return -1; - - //Start Janus API init. Need to get a session_id and handle_id - curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); - res = curl_easy_perform(curl); - if (res != CURLE_OK) return -1; - - pos = response.find("\"id\": "); - if (pos == std::string::npos) return -1; - std::string janus_id = response.substr(pos + 6, 16); - response = ""; endpoint += "/"; - endpoint += janus_id; - postData = "{\"janus\" : \"attach\", \"plugin\" : \"janus.plugin.streaming\", \"transaction\" : \"randomString\"}"; - curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); - res = curl_easy_perform(curl); - if (res != CURLE_OK) return -1; - - pos = response.find("\"id\": "); - if (pos == std::string::npos) return -1; - std::string handle_id = response.substr(pos + 6, 16); - endpoint += "/"; - endpoint += handle_id; + endpoint += janus_session; //Assemble our actual request postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; @@ -3604,9 +3542,75 @@ int Monitor::remove_from_janus() { curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); res = curl_easy_perform(curl); - if (res != CURLE_OK) return -1; + if (res != CURLE_OK) { + Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); + curl_easy_cleanup(curl); + return -1; + } Debug(1, "Removed stream from Janus: %s", response.c_str()); curl_easy_cleanup(curl); return 0; } + +int Monitor::get_janus_session() { + std::string response; + std::string endpoint; + if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) { + endpoint = config.janus_path; + } else { + endpoint = "127.0.0.1:8088/janus/"; + } + std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; + std::size_t pos; + CURLcode res; + curl = curl_easy_init(); + if(!curl) return -1; + + //Start Janus API init. Need to get a session_id and handle_id + curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); + curl_easy_cleanup(curl); + return -1; + } + + pos = response.find("\"id\": "); + if (pos == std::string::npos) + { + curl_easy_cleanup(curl); + return -1; + } + janus_session = response.substr(pos + 6, 16); + + response = ""; + endpoint += "/"; + endpoint += janus_session; + postData = "{\"janus\" : \"attach\", \"plugin\" : \"janus.plugin.streaming\", \"transaction\" : \"randomString\"}"; + curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) + { + Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); + curl_easy_cleanup(curl); + return -1; + } + + pos = response.find("\"id\": "); + if (pos == std::string::npos) + { + curl_easy_cleanup(curl); + return -1; + } + janus_session += "/"; + janus_session += response.substr(pos + 6, 16); + curl_easy_cleanup(curl); + return 1; +} //get_janus_session diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 381855c97..98c36e5a9 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -455,6 +455,8 @@ protected: static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp); int add_to_janus(); int remove_from_janus(); + int get_janus_session(); + std::string janus_session; // Used in check signal uint8_t red_val; From 146ea4822d6986950c5b19741dd44c05ca0c4f1e Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Fri, 21 Jan 2022 09:44:44 -0500 Subject: [PATCH 41/48] Bump the db queue limit to 30 before we warn. I only have 1 server that gets over 20 and it is still ok. --- src/zm_db.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_db.cpp b/src/zm_db.cpp index f0b13d538..cc0797c12 100644 --- a/src/zm_db.cpp +++ b/src/zm_db.cpp @@ -251,7 +251,7 @@ void zmDbQueue::process() { mCondition.wait(lock); } while (!mQueue.empty()) { - if (mQueue.size() > 20) { + if (mQueue.size() > 30) { Logger *log = Logger::fetch(); Logger::Level db_level = log->databaseLevel(); log->databaseLevel(Logger::NOLOG); From 3cc243b9a8992a3a70a5e2bb67b61e9c3831a09c Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Mon, 24 Jan 2022 09:23:19 -0500 Subject: [PATCH 42/48] Don't freshing config when doing update. That is it's own command --- scripts/zmupdate.pl.in | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/zmupdate.pl.in b/scripts/zmupdate.pl.in index d096c70b5..f62b14316 100644 --- a/scripts/zmupdate.pl.in +++ b/scripts/zmupdate.pl.in @@ -444,11 +444,6 @@ if ( $version ) { print( "\nUpgrading database to version ".ZM_VERSION."\n" ); -# Update config first of all - migratePaths(); - ZoneMinder::Config::loadConfigFromDB(); - ZoneMinder::Config::saveConfigToDB(); - my $cascade = undef; if ( $cascade || $version eq "1.19.0" ) { # Patch the database From 26ae5052f4bd19e34857223887439b171e65a8ca Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 25 Jan 2022 11:55:21 -0500 Subject: [PATCH 43/48] Fix fail to get Sources in RTSP. the string msg although initially reserved to ZM_NETWORK_BUFSIZ, after use it's capacity is changed whatever it's contents are. So need to re-reserve. --- src/zm_comms.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/zm_comms.h b/src/zm_comms.h index 7e7329d5d..ac772ae26 100644 --- a/src/zm_comms.h +++ b/src/zm_comms.h @@ -245,6 +245,7 @@ class Socket : public CommsBase { } virtual ssize_t recv(std::string &msg) const { + msg.reserve(ZM_NETWORK_BUFSIZ); std::vector buffer(msg.capacity()); ssize_t nBytes; if ((nBytes = ::recv(mSd, buffer.data(), buffer.size(), 0)) < 0) { From cf82d767de9976d39f8f98a6cc5bfe29a4ce9416 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 25 Jan 2022 14:24:52 -0500 Subject: [PATCH 44/48] Remove the decoding code, just populate the av_packet. This fixes rtsp decoding because we weren't copying the decoded frame to shm raw image. --- dep/RtspServer | 2 +- src/zm_remote_camera_rtsp.cpp | 62 ++++++++++++++--------------------- 2 files changed, 25 insertions(+), 39 deletions(-) diff --git a/dep/RtspServer b/dep/RtspServer index cd7fd49be..1b40f1661 160000 --- a/dep/RtspServer +++ b/dep/RtspServer @@ -1 +1 @@ -Subproject commit cd7fd49becad6010a1b8466bfebbd93999a39878 +Subproject commit 1b40f1661f93f50fd5805f239d1e466a3bcf888f diff --git a/src/zm_remote_camera_rtsp.cpp b/src/zm_remote_camera_rtsp.cpp index 8cdecdf94..b49862845 100644 --- a/src/zm_remote_camera_rtsp.cpp +++ b/src/zm_remote_camera_rtsp.cpp @@ -168,8 +168,10 @@ int RemoteCameraRtsp::PrimeCapture() { } } // end foreach stream - if ( mVideoStreamId == -1 ) - Fatal("Unable to locate video stream"); + if ( mVideoStreamId == -1 ) { + Error("Unable to locate video stream"); + return -1; + } if ( mAudioStreamId == -1 ) Debug(3, "Unable to locate audio stream"); @@ -179,17 +181,22 @@ int RemoteCameraRtsp::PrimeCapture() { // Find the decoder for the video stream AVCodec *codec = avcodec_find_decoder(mVideoCodecContext->codec_id); - if ( codec == nullptr ) - Panic("Unable to locate codec %d decoder", mVideoCodecContext->codec_id); + if ( codec == nullptr ) { + Error("Unable to locate codec %d decoder", mVideoCodecContext->codec_id); + return -1; + } // Open codec - if ( avcodec_open2(mVideoCodecContext, codec, nullptr) < 0 ) - Panic("Can't open codec"); + if ( avcodec_open2(mVideoCodecContext, codec, nullptr) < 0 ) { + Error("Can't open codec"); + return -1; + } int pSize = av_image_get_buffer_size(imagePixFormat, width, height, 1); if ( (unsigned int)pSize != imagesize ) { - Fatal("Image size mismatch. Required: %d Available: %llu", pSize, imagesize); + Error("Image size mismatch. Required: %d Available: %llu", pSize, imagesize); + return -1; } return 1; @@ -208,18 +215,13 @@ int RemoteCameraRtsp::PreCapture() { int RemoteCameraRtsp::Capture(std::shared_ptr &zm_packet) { int frameComplete = false; AVPacket *packet = &zm_packet->packet; - if ( !zm_packet->image ) { - Debug(1, "Allocating image %dx%d %d colours %d", width, height, colours, subpixelorder); - zm_packet->image = new Image(width, height, colours, subpixelorder); - } - while (!frameComplete) { buffer.clear(); if (!rtspThread || rtspThread->IsStopped()) return -1; - if ( rtspThread->getFrame(buffer) ) { + if (rtspThread->getFrame(buffer)) { Debug(3, "Read frame %d bytes", buffer.size()); Hexdump(4, buffer.head(), 16); @@ -254,36 +256,20 @@ int RemoteCameraRtsp::Capture(std::shared_ptr &zm_packet) { //while ( (!frameComplete) && (buffer.size() > 0) ) { if ( buffer.size() > 0 ) { - packet->data = buffer.head(); + packet->data = (uint8_t*)av_malloc(buffer.size()); + memcpy(packet->data, buffer.head(), buffer.size()); + //packet->data = buffer.head(); packet->size = buffer.size(); bytes += packet->size; + buffer -= packet->size; struct timeval now; - gettimeofday(&now, NULL); + gettimeofday(&now, nullptr); packet->pts = packet->dts = now.tv_sec*1000000+now.tv_usec; - - int bytes_consumed = zm_packet->decode(mVideoCodecContext); - if ( bytes_consumed < 0 ) { - Error("Error while decoding frame %d", frameCount); - //Hexdump(Logger::ERROR, buffer.head(), buffer.size()>256?256:buffer.size()); - } - buffer -= packet->size; - if ( bytes_consumed ) { - zm_dump_video_frame(zm_packet->in_frame, "remote_rtsp_decode"); - if (!mVideoStream->codecpar->width) { - zm_dump_codec(mVideoCodecContext); - zm_dump_codecpar(mVideoStream->codecpar); - mVideoStream->codecpar->width = zm_packet->in_frame->width; - mVideoStream->codecpar->height = zm_packet->in_frame->height; - zm_dump_codecpar(mVideoStream->codecpar); - } - zm_packet->codec_type = mVideoCodecContext->codec_type; - zm_packet->stream = mVideoStream; - frameComplete = true; - Debug(2, "Frame: %d - %d/%d", frameCount, bytes_consumed, buffer.size()); - packet->data = nullptr; - packet->size = 0; - } + zm_packet->codec_type = mVideoCodecContext->codec_type; + zm_packet->stream = mVideoStream; + frameComplete = true; + Debug(2, "Frame: %d - %d/%d", frameCount, packet->size, buffer.size()); } } /* getFrame() */ } // end while true From c8c09e560f071d2a5bb7ba4a04447c1a765e7879 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 25 Jan 2022 14:25:13 -0500 Subject: [PATCH 45/48] Fix mTerminate not being initialised. --- src/zm_rtp_source.cpp | 4 +++- src/zm_rtp_source.h | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/zm_rtp_source.cpp b/src/zm_rtp_source.cpp index 1862c1886..56ca2cf0d 100644 --- a/src/zm_rtp_source.cpp +++ b/src/zm_rtp_source.cpp @@ -45,8 +45,10 @@ RtpSource::RtpSource( mFrame(65536), mFrameCount(0), mFrameGood(true), + prevM(false), mFrameReady(false), - mFrameProcessed(false) + mFrameProcessed(false), + mTerminate(false) { char hostname[256] = ""; gethostname(hostname, sizeof(hostname)); diff --git a/src/zm_rtp_source.h b/src/zm_rtp_source.h index a39e8225f..71be9af2c 100644 --- a/src/zm_rtp_source.h +++ b/src/zm_rtp_source.h @@ -91,8 +91,6 @@ private: bool mFrameGood; bool prevM; - bool mTerminate; - bool mFrameReady; std::condition_variable mFrameReadyCv; std::mutex mFrameReadyMutex; @@ -100,6 +98,7 @@ private: bool mFrameProcessed; std::condition_variable mFrameProcessedCv; std::mutex mFrameProcessedMutex; + bool mTerminate; private: void init(uint16_t seq); From 38da3b4d52dc21c2443bf4a295cd0c636f5683e7 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 25 Jan 2022 14:25:27 -0500 Subject: [PATCH 46/48] add some brackets to make logic more clear --- src/zm_rtp_ctrl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_rtp_ctrl.cpp b/src/zm_rtp_ctrl.cpp index 25d34f0ff..a82ff2b2a 100644 --- a/src/zm_rtp_ctrl.cpp +++ b/src/zm_rtp_ctrl.cpp @@ -277,7 +277,7 @@ void RtpCtrlThread::Run() { TimePoint last_receive = std::chrono::steady_clock::now(); bool timeout = false; // used as a flag that we had a timeout, and then sent an RR to see if we wake back up. Real timeout will happen when this is true. - while (!mTerminate && select.wait() >= 0) { + while (!mTerminate && (select.wait() >= 0)) { TimePoint now = std::chrono::steady_clock::now(); zm::Select::CommsList readable = select.getReadable(); if ( readable.size() == 0 ) { From 4d90a816f86730ec1f6336f9e27eef0abffba2c6 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 25 Jan 2022 16:22:33 -0500 Subject: [PATCH 47/48] Put a lock around jpeg writing. libjpeg is not thread safe --- src/zm_image.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/zm_image.cpp b/src/zm_image.cpp index a4aad8b61..7260791a5 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -24,6 +24,7 @@ #include "zm_utils.h" #include #include +#include #include #include @@ -80,6 +81,8 @@ imgbufcpy_fptr_t fptr_imgbufcpy; /* Font */ static ZmFont font; +std::mutex jpeg_mutex; + void Image::update_function_pointers() { /* Because many loops are unrolled and work on 16 colours/time or 4 pixels/time, we have to meet requirements */ if ( pixels % 16 || pixels % 12 ) { @@ -1083,6 +1086,9 @@ bool Image::WriteJpeg(const std::string &filename, const int &quality_override, SystemTimePoint timestamp, bool on_blocking_abort) const { + // jpeg libs are not thread safe + std::unique_lock lck(jpeg_mutex); + if (config.colour_jpeg_files && (colours == ZM_COLOUR_GRAY8)) { Image temp_image(*this); temp_image.Colourise(ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB); @@ -1366,6 +1372,8 @@ bool Image::EncodeJpeg(JOCTET *outbuffer, int *outbuffer_size, int quality_overr return temp_image.EncodeJpeg(outbuffer, outbuffer_size, quality_override); } + std::unique_lock lck(jpeg_mutex); + int quality = quality_override ? quality_override : config.jpeg_stream_quality; struct jpeg_compress_struct *cinfo = encodejpg_ccinfo[quality]; From 961256d2e78bb6c84912e621ca0fdb0b9b6cd54d Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 25 Jan 2022 16:24:21 -0500 Subject: [PATCH 48/48] terminate when zm_terminate is set. Do a countdown instead of countup. Sleeo for 10000 microseconds instead of 100. This restores the old value --- src/zm_remote_camera_rtsp.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/zm_remote_camera_rtsp.cpp b/src/zm_remote_camera_rtsp.cpp index b49862845..ffb4a061d 100644 --- a/src/zm_remote_camera_rtsp.cpp +++ b/src/zm_remote_camera_rtsp.cpp @@ -22,6 +22,7 @@ #include "zm_config.h" #include "zm_monitor.h" #include "zm_packet.h" +#include "zm_signal.h" RemoteCameraRtsp::RemoteCameraRtsp( const Monitor *monitor, @@ -126,8 +127,8 @@ int RemoteCameraRtsp::Disconnect() { int RemoteCameraRtsp::PrimeCapture() { Debug(2, "Waiting for sources"); - for (int i = 0; i < 100 && !rtspThread->hasSources(); i++) { - std::this_thread::sleep_for(Microseconds(100)); + for (int i = 100; i && !zm_terminate && !rtspThread->hasSources(); i--) { + std::this_thread::sleep_for(Microseconds(10000)); } if (!rtspThread->hasSources()) { @@ -218,7 +219,7 @@ int RemoteCameraRtsp::Capture(std::shared_ptr &zm_packet) { while (!frameComplete) { buffer.clear(); - if (!rtspThread || rtspThread->IsStopped()) + if (!rtspThread || rtspThread->IsStopped() || zm_terminate) return -1; if (rtspThread->getFrame(buffer)) {