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 diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index 8c0d4609c..fb2b8cf21 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@; @@ -460,6 +461,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.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/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/distros/redhat/zoneminder.spec b/distros/redhat/zoneminder.spec index 33996f755..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.8 +Version: 1.37.10 Release: 1%{?dist} Summary: A camera monitoring and analysis tool Group: System Environment/Daemons 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..dc50e8d64 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', }, { @@ -370,6 +371,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', @@ -462,6 +485,7 @@ our @options = ( {name=>'ZM_OPT_USE_GOOG_RECAPTCHA', value=>'yes'} ], type => $types{string}, + private => 1, category => 'system', }, { @@ -477,6 +501,7 @@ our @options = ( {name=>'ZM_OPT_USE_GOOG_RECAPTCHA', value=>'yes'} ], type => $types{string}, + private => 1, category => 'system', }, { 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 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) { 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); 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); diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 1c58130cc..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 ) { @@ -114,22 +117,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 +162,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 +178,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; } @@ -1082,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); @@ -1365,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]; 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 24160c11f..0c497e951 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #if ZM_MEM_MAPPED #include @@ -78,7 +79,7 @@ struct Namespace namespaces[] = // 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`, `Capturing`+0, `Analysing`+0, `AnalysisSource`, `Recording`+0, `RecordingSource`, `DecodingEnabled`, " -"`JanusEnabled`," +"`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`, " @@ -319,6 +320,7 @@ Monitor::Monitor() recording(RECORDING_ALWAYS), decoding_enabled(false), janus_enabled(false), + janus_audio_enabled(false), //protocol //method //options @@ -458,9 +460,10 @@ Monitor::Monitor() } // Monitor::Monitor /* +<<<<<<< HEAD std::string load_monitor_sql = "SELECT `Id`, `Name`, `ServerId`, `StorageId`, `Type`, `Capturing`+0, `Analysing`+0, `AnalysisSource`, `Recording`+0, `RecordingSource`, - `DecodingEnabled`, JanusEnabled, " + `DecodingEnabled`, JanusEnabled, JanusAudioEnabled, " "LinkedMonitors, `EventStartCommand`, `EventEndCommand`, " "AnalysisFPSLimit, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS," "Device, Channel, Format, V4LMultiBuffer, V4LCapturesPerFrame, " // V4L Settings @@ -519,6 +522,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++; @@ -1138,12 +1142,14 @@ 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) { - if (add_to_janus() != 0) { - 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 + if (janus_enabled) + Error("zmc not compiled with LIBCURL. Janus support not built in!"); #endif } else if (!shared_data->valid) { @@ -1166,6 +1172,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)); } @@ -1808,6 +1815,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) { @@ -1849,10 +1858,17 @@ 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 (janus_session.empty()) { + get_janus_session(); + } + if (check_janus() == 0) { + add_to_janus(); + } + } + std::this_thread::sleep_until(loop_start_time + std::chrono::seconds(5)); return TRUE; } //end Poll @@ -2084,7 +2100,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 @@ -2099,7 +2115,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) { @@ -2222,7 +2238,7 @@ bool Monitor::Analyse() { 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()), @@ -2562,8 +2578,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); @@ -2694,7 +2708,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; @@ -3167,10 +3181,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); @@ -3178,7 +3190,7 @@ int Monitor::PrimeCapture() { Poller->Start(); } } -#endif + if (decoding_enabled) { if (!decoder_it) decoder_it = packetqueue.get_video_it(false); if (!decoder) { @@ -3382,20 +3394,26 @@ 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; std::string rtsp_path = "rtsp://"; - std::string janus_id; std::size_t pos; std::size_t pos2; 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; @@ -3407,36 +3425,15 @@ 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) return -1; - - pos = response.find("\"id\": "); - if (pos == std::string::npos) return -1; - janus_id = response.substr(pos + 6, 16); - response = ""; - 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); //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\" : {"; - 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; @@ -3444,6 +3441,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()); @@ -3451,53 +3449,44 @@ 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; + } + 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; } -int Monitor::remove_from_janus() { - //TODO clean this up, add error checking, etc + +int Monitor::check_janus() { std::string response; - std::string endpoint = "127.0.0.1:8088/janus/"; - std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; - std::size_t pos; + 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; + //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 += 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\" : {"; - postData += "\"request\" : \"destroy\", \"admin_key\" : \"supersecret\", \"id\" : "; + postData += "\"request\" : \"info\", \"id\" : "; postData += std::to_string(id); postData += "}}"; @@ -3506,9 +3495,128 @@ 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) { //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; + } + + curl_easy_cleanup(curl); + 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; + 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; + + endpoint += "/"; + endpoint += janus_session; + + //Assemble our actual request + postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; + postData += "\"request\" : \"destroy\", \"admin_key\" : \""; + postData += config.janus_secret; + postData += "\", \"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) { + 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 dc3f56701..0042a9774 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -308,6 +308,7 @@ protected: 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; @@ -494,6 +495,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; @@ -554,6 +557,21 @@ 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; + } + int check_janus(); //returns 1 for healthy, 0 for success but missing stream, negative for error. +#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) { diff --git a/src/zm_monitorstream.cpp b/src/zm_monitorstream.cpp index c2e58d8c5..39724b63d 100644 --- a/src/zm_monitorstream.cpp +++ b/src/zm_monitorstream.cpp @@ -734,12 +734,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]; } diff --git a/src/zm_remote_camera_rtsp.cpp b/src/zm_remote_camera_rtsp.cpp index 8cdecdf94..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()) { @@ -168,8 +169,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 +182,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 +216,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()) + if (!rtspThread || rtspThread->IsStopped() || zm_terminate) 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 +257,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 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 ) { 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); diff --git a/version b/version index 21b6230a1..a9009bf2a 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.37.8 +1.37.10 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'].'
'; + } +?> + -
-  -  fps -
+ getMonitorStateHTML(); +?> +