diff --git a/.eslintrc.js b/.eslintrc.js index 0bb050ad1..16cbb37ba 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,7 +3,7 @@ module.exports = { "env": { "browser": true, - "es6": true, + "es2017": true, }, "extends": ["google"], "overrides": [{ diff --git a/CMakeLists.txt b/CMakeLists.txt index 0479ce1fd..7df6aaa70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -321,7 +321,7 @@ if(NOT ZM_NO_CURL) find_package(CURL) if(CURL_FOUND) set(HAVE_LIBCURL 1) - #list(APPEND ZM_BIN_LIBS ${CURL_LIBRARIES}) + list(APPEND ZM_BIN_LIBS ${CURL_LIBRARIES}) include_directories(${CURL_INCLUDE_DIRS}) set(CMAKE_REQUIRED_INCLUDES ${CURL_INCLUDE_DIRS}) check_include_file("curl/curl.h" HAVE_CURL_CURL_H) diff --git a/README.md b/README.md index 2249f1752..da96b3e60 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ ZoneMinder [![Build Status](https://travis-ci.org/ZoneMinder/zoneminder.png)](https://travis-ci.org/ZoneMinder/zoneminder) [![Bounty Source](https://api.bountysource.com/badge/team?team_id=204&style=bounties_received)](https://www.bountysource.com/teams/zoneminder/issues?utm_source=ZoneMinder&utm_medium=shield&utm_campaign=bounties_received) [![Join Slack](https://github.com/ozonesecurity/ozonebase/blob/master/img/slacksm.png?raw=true)](https://join.slack.com/t/zoneminder-chat/shared_invite/enQtNTU0NDkxMDM5NDQwLTdhZmQ5Y2M2NWQyN2JkYTBiN2ZkMzIzZGQ0MDliMTRmM2FjZWRlYzUwYTQ2MjMwMTVjMzQ1NjYxOTdmMjE2MTE) -[![IRC Network](https://img.shields.io/badge/irc-%23zoneminder-blue.svg "IRC Freenode")](https://webchat.freenode.net/?channels=zoneminder) All documentation for ZoneMinder is now online at https://zoneminder.readthedocs.org diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index f20e67a4f..8c0d4609c 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -459,6 +459,7 @@ CREATE TABLE `Monitors` ( `Recording` enum('None', 'OnMotion', 'Always') NOT NULL default 'Always', `Enabled` tinyint(3) unsigned NOT NULL default '1', `DecodingEnabled` tinyint(3) unsigned NOT NULL default '1', + `JanusEnabled` 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.8.sql b/db/zm_update-1.37.8.sql new file mode 100644 index 000000000..650489e7b --- /dev/null +++ b/db/zm_update-1.37.8.sql @@ -0,0 +1,18 @@ +-- +-- Update Monitors table to have JanusEnabled +-- + +SELECT 'Checking for JanusEnabled in Monitors'; +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Monitors' + AND table_schema = DATABASE() + AND column_name = 'JanusEnabled' + ) > 0, +"SELECT 'Column JanusEnabled already exists in Monitors'", +"ALTER TABLE `Monitors` ADD COLUMN `JanusEnabled` BOOLEAN NOT NULL default false AFTER `DecodingEnabled`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/misc/janus.jcfg b/misc/janus.jcfg new file mode 100644 index 000000000..a0adffb88 --- /dev/null +++ b/misc/janus.jcfg @@ -0,0 +1,50 @@ +general: { + configs_folder = "/usr/local/etc/janus" # Configuration files folder + plugins_folder = "/usr/local/lib/janus/plugins" # Plugins folder + transports_folder = "/usr/local/lib/janus/transports" # Transports folder + events_folder = "/usr/local/lib/janus/events" # Event handlers folder + loggers_folder = "/usr/local/lib/janus/loggers" # External loggers folder + debug_level = 4 # Debug/logging level, valid values are 0-7 + admin_secret = "janusoverlord" # String that all Janus requests must contain + protected_folders = [ + "/bin", + "/boot", + "/dev", + "/etc", + "/initrd", + "/lib", + "/lib32", + "/lib64", + "/proc", + "/sbin", + "/sys", + "/usr", + "/var", + "/opt/janus/bin", + "/opt/janus/etc", + "/opt/janus/include", + "/opt/janus/lib", + "/opt/janus/lib32", + "/opt/janus/lib64", + "/opt/janus/sbin" + ] +} +media: { + #ipv6 = true + #ipv6_linklocal = true + rtp_port_range = "20000-40000" +} +nat: { + nice_debug = false + ignore_mdns = true + keep_private_host = true + ice_ignore_list = "vmnet" +} + +plugins: { + disable = "libjanus_audiobridge.so,libjanus_echotest.so,libjanus_recordplay.so,libjanus_sip.so,libjanus_textroom.so,libjanus_videocall.so,libjanus_videoroom.so,libjanus_voicemail.so, + libjanus_nosip.so" +} +transports: { + disable = "libjanus_rabbitmq.so, libjanus_pfunix.so,libjanus_websockets.so" +} diff --git a/misc/janus.plugin.streaming.jcfg b/misc/janus.plugin.streaming.jcfg new file mode 100644 index 000000000..f93b15a3e --- /dev/null +++ b/misc/janus.plugin.streaming.jcfg @@ -0,0 +1,4 @@ +general: { + admin_key = "supersecret" + rtp_port_range = "20000-40000" +} diff --git a/misc/janus.transport.http.jcfg b/misc/janus.transport.http.jcfg new file mode 100644 index 000000000..8ae9171ad --- /dev/null +++ b/misc/janus.transport.http.jcfg @@ -0,0 +1,25 @@ +general: { + json = "indented" # Whether the JSON messages should be indented (default), + base_path = "/janus" # Base path to bind to in the web server (plain HTTP only) + http = true # Whether to enable the plain HTTP interface + port = 8088 # Web server HTTP port + https = false # Whether to enable HTTPS (default=false) +} + +# Janus can also expose an admin/monitor endpoint, to allow you to check +# which sessions are up, which handles they're managing, their current +# status and so on. This provides a useful aid when debugging potential +# issues in Janus. The configuration is pretty much the same as the one +# already presented above for the webserver stuff, as the API is very +# similar: choose the base bath for the admin/monitor endpoint (/admin +# by default), ports, etc. Besides, you can specify +# a secret that must be provided in all requests as a crude form of +# authorization mechanism, and partial or full source IPs if you want to +# limit access basing on IP addresses. For security reasons, this +# endpoint is disabled by default, enable it by setting admin_http=true. +admin: { + admin_base_path = "/admin" # Base path to bind to in the admin/monitor web server (plain HTTP only) + admin_http = false # Whether to enable the plain HTTP interface + admin_port = 7088 # Admin/monitor web server HTTP port + admin_https = false # Whether to enable HTTPS (default=false) +} diff --git a/src/zm_event.h b/src/zm_event.h index be6d922de..31a23fad4 100644 --- a/src/zm_event.h +++ b/src/zm_event.h @@ -132,6 +132,7 @@ class Event { SystemTimePoint StartTime() const { return start_time; } SystemTimePoint EndTime() const { return end_time; } + TimePoint::duration Duration() const { return end_time - start_time; }; void AddPacket(const std::shared_ptr &p); void AddPacket_(const std::shared_ptr &p); @@ -178,18 +179,6 @@ class Event { return pre_alarm_count; } static void EmptyPreAlarmFrames() { -#if 0 - while ( pre_alarm_count > 0 ) { - int i = pre_alarm_count - 1; - delete pre_alarm_data[i].image; - pre_alarm_data[i].image = nullptr; - if ( pre_alarm_data[i].alarm_frame ) { - delete pre_alarm_data[i].alarm_frame; - pre_alarm_data[i].alarm_frame = nullptr; - } - pre_alarm_count--; - } -#endif pre_alarm_count = 0; } static void AddPreAlarmFrame( @@ -198,28 +187,10 @@ class Event { int score=0, Image *alarm_frame=nullptr ) { -#if 0 - pre_alarm_data[pre_alarm_count].image = new Image(*image); - pre_alarm_data[pre_alarm_count].timestamp = timestamp; - pre_alarm_data[pre_alarm_count].score = score; - if ( alarm_frame ) { - pre_alarm_data[pre_alarm_count].alarm_frame = new Image(*alarm_frame); - } -#endif pre_alarm_count++; } void SavePreAlarmFrames() { -#if 0 - for ( int i = 0; i < pre_alarm_count; i++ ) { - AddFrame( - pre_alarm_data[i].image, - pre_alarm_data[i].timestamp, - pre_alarm_data[i].score, - pre_alarm_data[i].alarm_frame); - } -#endif EmptyPreAlarmFrames(); } }; - #endif // ZM_EVENT_H diff --git a/src/zm_eventstream.cpp b/src/zm_eventstream.cpp index 927c3b3aa..54f6b60d8 100644 --- a/src/zm_eventstream.cpp +++ b/src/zm_eventstream.cpp @@ -1126,7 +1126,8 @@ bool EventStream::send_file(const std::string &filepath) { } else { Debug(1, "Failed to sendfile?"); } - Warning("Unable to send raw frame %ld: %s rc %d", curr_frame_id, strerror(errno), rc); + Warning("Unable to send raw frame %ld: %s rc %d != %d", + curr_frame_id, strerror(errno), rc, (int)filestat.st_size); #endif static unsigned char temp_img_buffer[ZM_MAX_IMAGE_SIZE]; diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 73c54d742..fd16d3166 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -77,7 +77,9 @@ 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 = +<<<<<<< HEAD "SELECT `Id`, `Name`, `ServerId`, `StorageId`, `Type`, `Capturing`+0, `Analysing`+0, `AnalysisSource`, `Recording`+0, `RecordingSource`, `DecodingEnabled`, " +"`JanusEnabled`," "`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`, " @@ -318,6 +320,7 @@ Monitor::Monitor() analysing(ANALYSING_ALWAYS), recording(RECORDING_ALWAYS), decoding_enabled(false), + janus_enabled(false), //protocol //method //options @@ -457,23 +460,23 @@ Monitor::Monitor() } // Monitor::Monitor /* - std::string load_monitor_sql = - "SELECT `Id`, `Name`, `ServerId`, `StorageId`, `Type`, `Capturing`+0, `Analysing`+0, `AnalysisSource`, `Recording`+0, `RecordingSource`, - `DecodingEnabled`, " - "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, " - "SaveJPEGs, VideoWriter, EncoderParameters, - "OutputCodec, Encoder, OutputContainer, RecordAudio, " - "Brightness, Contrast, Hue, Colour, " - "EventPrefix, LabelFormat, LabelX, LabelY, LabelSize," - "ImageBufferCount, `MaxImageBufferCount`, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, " - "SectionLength, MinSectionLength, FrameSkip, MotionFrameSkip, " - "FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif," - "`RTSPServer`,`RTSPStreamName`, - "`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, " - "SignalCheckPoints, SignalCheckColour, Importance-1 FROM Monitors"; + std::string load_monitor_sql = + "SELECT `Id`, `Name`, `ServerId`, `StorageId`, `Type`, `Capturing`+0, `Analysing`+0, `AnalysisSource`, `Recording`+0, `RecordingSource`, + `DecodingEnabled`, JanusEnabled, " + "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, " + "SaveJPEGs, VideoWriter, EncoderParameters, + "OutputCodec, Encoder, OutputContainer, RecordAudio, " + "Brightness, Contrast, Hue, Colour, " + "EventPrefix, LabelFormat, LabelX, LabelY, LabelSize," + "ImageBufferCount, `MaxImageBufferCount`, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, " + "SectionLength, MinSectionLength, FrameSkip, MotionFrameSkip, " + "FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif," + "`RTSPServer`,`RTSPStreamName`, + "`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, " + "SignalCheckPoints, SignalCheckColour, Importance-1 FROM Monitors"; */ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { @@ -517,6 +520,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++; ReloadLinkedMonitors(dbrow[col]); col++; event_start_command = dbrow[col] ? dbrow[col] : ""; col++; @@ -1092,44 +1096,55 @@ bool Monitor::connect() { usedsubpixorder = camera->SubpixelOrder(); // Used in CheckSignal shared_data->valid = true; - //ONVIF Setup #ifdef WITH_GSOAP - ONVIF_Trigger_State = FALSE; - if (onvif_event_listener) { - Debug(1, "Starting ONVIF"); - ONVIF_Healthy = FALSE; - if (onvif_options.find("closes_event") != std::string::npos) { //Option to indicate that ONVIF will send a close event message - ONVIF_Closes_Event = TRUE; - } - tev__PullMessages.Timeout = "PT600S"; - tev__PullMessages.MessageLimit = 100; - soap = soap_new(); - soap->connect_timeout = 5; - soap->recv_timeout = 5; - soap->send_timeout = 5; - soap_register_plugin(soap, soap_wsse); - proxyEvent = PullPointSubscriptionBindingProxy(soap); - std::string full_url = onvif_url + "/Events"; - proxyEvent.soap_endpoint = full_url.c_str(); - set_credentials(soap); - Debug(1, "ONVIF Endpoint: %s", proxyEvent.soap_endpoint); - if (proxyEvent.CreatePullPointSubscription(&request, response) != SOAP_OK) { - Warning("Couldn't create subscription!"); - } else { - //Empty the stored messages + + //ONVIF Setup + ONVIF_Trigger_State = FALSE; + if (onvif_event_listener) { //Temporarily using this option to enable the feature + Debug(1, "Starting ONVIF"); + ONVIF_Healthy = FALSE; + if (onvif_options.find("closes_event") != std::string::npos) { //Option to indicate that ONVIF will send a close event message + ONVIF_Closes_Event = TRUE; + } + tev__PullMessages.Timeout = "PT600S"; + tev__PullMessages.MessageLimit = 100; + soap = soap_new(); + soap->connect_timeout = 5; + soap->recv_timeout = 5; + soap->send_timeout = 5; + soap_register_plugin(soap, soap_wsse); + proxyEvent = PullPointSubscriptionBindingProxy(soap); + std::string full_url = onvif_url + "/Events"; + proxyEvent.soap_endpoint = full_url.c_str(); + set_credentials(soap); - if (proxyEvent.PullMessages(response.SubscriptionReference.Address, NULL, &tev__PullMessages, tev__PullMessagesResponse) != SOAP_OK) { - Warning("Couldn't do initial event pull! %s", response.SubscriptionReference.Address); + Debug(1, "ONVIF Endpoint: %s", proxyEvent.soap_endpoint); + if (proxyEvent.CreatePullPointSubscription(&request, response) != SOAP_OK) { + Warning("Couldn't create subscription!"); } else { - Debug(1, "Good Initial ONVIF Pull"); - ONVIF_Healthy = TRUE; + //Empty the stored messages + set_credentials(soap); + if (proxyEvent.PullMessages(response.SubscriptionReference.Address, NULL, &tev__PullMessages, tev__PullMessagesResponse) != SOAP_OK) { + Warning("Couldn't do initial event pull! %s", response.SubscriptionReference.Address); + } else { + Debug(1, "Good Initial ONVIF Pull"); + ONVIF_Healthy = TRUE; + } + } + } else { + Debug(1, "Not Starting ONVIF"); + } + //End ONVIF Setup +#endif + +#if HAVE_LIBCURL //janus setup. Depends on libcurl. + if (janus_enabled && (path.find("rtsp://") != std::string::npos)) { + if (add_to_janus() != 0) { + Warning("Failed to add monitor stream to Janus!"); } } - } else { - Debug(1, "Not Starting ONVIF"); - } - //End ONVIF Setup #endif + } else if (!shared_data->valid) { Error("Shared data not initialised by capture daemon for monitor %s", name.c_str()); return false; @@ -1856,6 +1871,12 @@ bool Monitor::Analyse() { packetqueue.increment_it(analysis_it); return false; } + // Ready means that we have captured the warmup # of frames + if (!Ready()) { + Debug(3, "Not ready?"); + delete packet_lock; + return false; + } // signal is set by capture bool signal = shared_data->signal; @@ -1864,410 +1885,383 @@ bool Monitor::Analyse() { Debug(3, "Motion detection is enabled signal(%d) signal_change(%d) trigger state(%s) image index %d", signal, signal_change, TriggerState_Strings[trigger_data->trigger_state].c_str(), snap->image_index); - // Need to guard around event creation/deletion from Reload() - std::lock_guard lck(event_mutex); + { // scope for event lock + // Need to guard around event creation/deletion from Reload() + std::lock_guard lck(event_mutex); - // if we have been told to be OFF, then we are off and don't do any processing. - if (trigger_data->trigger_state != TriggerState::TRIGGER_OFF) { - Debug(4, "Trigger not OFF state is (%d)", int(trigger_data->trigger_state)); - // Ready means that we have captured the warmup # of frames - if (!Ready()) { - Debug(3, "Not ready?"); - delete packet_lock; - return false; - } + // if we have been told to be OFF, then we are off and don't do any processing. + if (trigger_data->trigger_state != TriggerState::TRIGGER_OFF) { + Debug(4, "Trigger not OFF state is (%d)", int(trigger_data->trigger_state)); - int score = 0; - std::string cause; - Event::StringSetMap noteSetMap; + int score = 0; + std::string cause; + Event::StringSetMap noteSetMap; #ifdef WITH_GSOAP - if (ONVIF_Trigger_State) { - score += 9; - Debug(1, "Triggered on ONVIF"); - if (!event) { - Event::StringSet noteSet; - noteSet.insert("ONVIF2"); - noteSetMap[MOTION_CAUSE] = noteSet; - - event = openEvent(snap, "ONVIF", noteSetMap); - cause += "ONVIF"; - } else { - event->addNote(MOTION_CAUSE, "ONVIF2"); -// Add to cause - } - // Regardless of previous state, we go to ALARM - shared_data->state = state = ALARM; - //If the camera isn't going to send an event close, we need to close it here, but only after it has actually triggered an alarm. - if (!ONVIF_Closes_Event && state == ALARM) - ONVIF_Trigger_State = FALSE; - } // end ONVIF_Trigger + if (onvif_event_listener && ONVIF_Healthy) { + if (ONVIF_Trigger_State) { + score += 9; + Debug(1, "Triggered on ONVIF"); + Event::StringSet noteSet; + noteSet.insert("ONVIF2"); + noteSetMap[MOTION_CAUSE] = noteSet; + cause += "ONVIF"; + //If the camera isn't going to send an event close, we need to close it here, but only after it has actually triggered an alarm. + if (!ONVIF_Closes_Event && state == ALARM) + ONVIF_Trigger_State = FALSE; + } // end ONVIF_Trigger + } // end if (onvif_event_listener && ONVIF_Healthy) #endif - // Specifically told to be on. Setting the score here will trigger the alarm. - if (trigger_data->trigger_state == TriggerState::TRIGGER_ON) { - score += trigger_data->trigger_score; - Debug(1, "Triggered on score += %d => %d", trigger_data->trigger_score, score); - if (!event) { + // Specifically told to be on. Setting the score here is not enough to trigger the alarm. Must jump directly to ALARM + if (trigger_data->trigger_state == TriggerState::TRIGGER_ON) { + score += trigger_data->trigger_score; + Debug(1, "Triggered on score += %d => %d", trigger_data->trigger_score, score); + if (!cause.empty()) cause += ", "; + cause += trigger_data->trigger_cause; Event::StringSet noteSet; noteSet.insert(trigger_data->trigger_text); noteSetMap[trigger_data->trigger_cause] = noteSet; - event = openEvent(snap, trigger_data->trigger_cause, noteSetMap); - Info("%s: %03d - Opening new event %" PRIu64 ", alarm start", name.c_str(), analysis_image_count, event->Id()); - } else { - event->addNote(trigger_data->trigger_cause, trigger_data->trigger_text); - // Need to know if we should end the previous and start a new one, or just add the data - } - shared_data->state = state = ALARM; - } // end if trigger_on + shared_data->state = state = ALARM; + } // end if trigger_on - // FIXME this snap might not be the one that caused the signal change. Need to store that in the packet. - if (signal_change) { - Debug(2, "Signal change, new signal is %d", signal); - if (!signal) { - if (event) { - event->addNote(SIGNAL_CAUSE, "Lost"); - Info("%s: %03d - Closing event %" PRIu64 ", signal loss", name.c_str(), analysis_image_count, event->Id()); - closeEvent(); + // FIXME this snap might not be the one that caused the signal change. Need to store that in the packet. + if (signal_change) { + Debug(2, "Signal change, new signal is %d", signal); + if (!signal) { + if (event) { + event->addNote(SIGNAL_CAUSE, "Lost"); + Info("%s: %03d - Closing event %" PRIu64 ", signal loss", name.c_str(), analysis_image_count, event->Id()); + closeEvent(); + } + } else if (recording == RECORDING_ALWAYS) { + if (!event) { + if (!cause.empty()) cause += ", "; + cause += SIGNAL_CAUSE + std::string(": Reacquired"); + } else { + event->addNote(SIGNAL_CAUSE, "Reacquired"); + } + if (snap->image) + ref_image.Assign(*(snap->image)); } - } else if (recording == RECORDING_ALWAYS) { - if (!event) { - if (!cause.empty()) cause += ", "; - cause += SIGNAL_CAUSE + std::string(": Reacquired"); - } else { - event->addNote(SIGNAL_CAUSE, "Reacquired"); - } - if (snap->image) - ref_image.Assign(*(snap->image)); - } - shared_data->state = state = IDLE; - } // end if signal change + shared_data->state = state = IDLE; + } // end if signal change - if (signal) { - if (snap->codec_type == AVMEDIA_TYPE_VIDEO) { - // Check to see if linked monitors are triggering. - if (n_linked_monitors > 0) { - Debug(1, "Checking linked monitors"); - // FIXME improve logic here - bool first_link = true; - Event::StringSet noteSet; - for (int i = 0; i < n_linked_monitors; i++) { - // TODO: Shouldn't we try to connect? - if (linked_monitors[i]->isConnected()) { - Debug(1, "Linked monitor %d %s is connected", - linked_monitors[i]->Id(), linked_monitors[i]->Name()); - if (linked_monitors[i]->hasAlarmed()) { - Debug(1, "Linked monitor %d %s is alarmed", + if (signal) { + if (snap->codec_type == AVMEDIA_TYPE_VIDEO) { + // Check to see if linked monitors are triggering. + if (n_linked_monitors > 0) { + Debug(1, "Checking linked monitors"); + // FIXME improve logic here + bool first_link = true; + Event::StringSet noteSet; + for (int i = 0; i < n_linked_monitors; i++) { + // TODO: Shouldn't we try to connect? + if (linked_monitors[i]->isConnected()) { + Debug(1, "Linked monitor %d %s is connected", linked_monitors[i]->Id(), linked_monitors[i]->Name()); - if (!event) { - if (first_link) { - if (cause.length()) - cause += ", "; - cause += LINKED_CAUSE; - first_link = false; + if (linked_monitors[i]->hasAlarmed()) { + Debug(1, "Linked monitor %d %s is alarmed", + linked_monitors[i]->Id(), linked_monitors[i]->Name()); + if (!event) { + if (first_link) { + if (cause.length()) + cause += ", "; + cause += LINKED_CAUSE; + first_link = false; + } + } + noteSet.insert(linked_monitors[i]->Name()); + score += linked_monitors[i]->lastFrameScore(); // 50; + } else { + Debug(1, "Linked monitor %d %s is not alarmed", + linked_monitors[i]->Id(), linked_monitors[i]->Name()); + } + } else { + Debug(1, "Linked monitor %d %d is not connected. Connecting.", i, linked_monitors[i]->Id()); + linked_monitors[i]->connect(); + } + } // end foreach linked_monitor + if (noteSet.size() > 0) + noteSetMap[LINKED_CAUSE] = noteSet; + } // end if linked_monitors + + /* try to stay behind the decoder. */ + if (decoding_enabled) { + while (!snap->decoded and !zm_terminate and !analysis_thread->Stopped()) { + // Need to wait for the decoder thread. + // decoder thread might be waiting on the lock for this packet. + // So we need to relinquish the lock and wait. Waiting automatically relinquishes the lock + // So... + Debug(1, "Waiting for decode"); + packet_lock->wait(); + //packetqueue.unlock(packet_lock); // This will delete packet_lock and notify_all + //packetqueue.wait(); + ////packet_lock->lock(); + } // end while ! decoded + if (zm_terminate or analysis_thread->Stopped()) { + delete packet_lock; + return false; + } + } // end if decoding enabled + + if (shared_data->analysing) { + Debug(3, "signal and capturing and doing motion detection"); + + if (analysis_fps_limit) { + double capture_fps = get_capture_fps(); + motion_frame_skip = capture_fps / analysis_fps_limit; + Debug(1, "Recalculating motion_frame_skip (%d) = capture_fps(%f) / analysis_fps(%f)", + motion_frame_skip, capture_fps, analysis_fps_limit); + } + + Event::StringSet zoneSet; + + if (snap->image) { + // decoder may not have been able to provide an image + if (!ref_image.Buffer()) { + Debug(1, "Assigning instead of Detecting"); + ref_image.Assign(*(snap->image)); + alarm_image.Assign(*(snap->image)); + } else if (!(analysis_image_count % (motion_frame_skip+1))) { + Debug(1, "Detecting motion on image %d, image %p", snap->image_index, snap->image); + // Get new score. + snap->score = DetectMotion(*(snap->image), zoneSet); + + if (!snap->analysis_image) + snap->analysis_image = new Image(*(snap->image)); + // lets construct alarm cause. It will contain cause + names of zones alarmed + snap->zone_stats.reserve(zones.size()); + for (const Zone &zone : zones) { + const ZoneStats &stats = zone.GetStats(); + stats.DumpToLog("After detect motion"); + snap->zone_stats.push_back(stats); + if (zone.Alarmed()) { + if (!snap->alarm_cause.empty()) snap->alarm_cause += ","; + snap->alarm_cause += std::string(zone.Label()); + if (zone.AlarmImage()) + snap->analysis_image->Overlay(*(zone.AlarmImage())); } } - noteSet.insert(linked_monitors[i]->Name()); - score += linked_monitors[i]->lastFrameScore(); // 50; + alarm_image.Assign(*(snap->analysis_image)); + Debug(3, "After motion detection, score:%d last_motion_score(%d), new motion score(%d)", + score, last_motion_score, snap->score); + motion_frame_count += 1; + last_motion_score = snap->score; + + if (snap->score) { + if (cause.length()) cause += ", "; + cause += MOTION_CAUSE + std::string(":") + snap->alarm_cause; + noteSetMap[MOTION_CAUSE] = zoneSet; + score += snap->score; + } // end if motion_score } else { - Debug(1, "Linked monitor %d %s is not alarmed", - linked_monitors[i]->Id(), linked_monitors[i]->Name()); + Debug(1, "Skipped motion detection last motion score was %d", last_motion_score); + alarm_image.Assign(*(snap->image)); } } else { - Debug(1, "Linked monitor %d %d is not connected. Connecting.", i, linked_monitors[i]->Id()); - linked_monitors[i]->connect(); - } - } // end foreach linked_monitor - if (noteSet.size() > 0) - noteSetMap[LINKED_CAUSE] = noteSet; - } // end if linked_monitors + Debug(1, "no image so skipping motion detection"); + } // end if has image + } else { + Debug(1, "Not analysing %d", shared_data->analysing); + } // end if active and doing motion detection - /* try to stay behind the decoder. */ - if (decoding_enabled) { - if (!snap->decoded and !zm_terminate and !analysis_thread->Stopped()) { - // Need to wait for the decoder thread. - Debug(1, "Waiting for decode"); - packetqueue.unlock(packet_lock); // This will delete packet_lock and notify_all - packetqueue.wait(); - // Everything may have changed, just return and start again. This needs to be more RAII - return true; - } // end while ! decoded - } // end if decoding enabled + Info("%s: %03d - Closing event %" PRIu64 ", section end forced %" PRIi64 " - %" PRIi64 " = %" PRIi64 " >= %" PRIi64 , + name.c_str(), + image_count, + 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()), + static_cast(std::chrono::duration_cast(snap->timestamp - event->StartTime()).count()), + static_cast(Seconds(section_length).count())); + closeEvent(); + } // end if section_length + } // end if event - if (shared_data->analysing) { - Debug(3, "signal and capturing and doing motion detection"); - - if (analysis_fps_limit) { - double capture_fps = get_capture_fps(); - motion_frame_skip = capture_fps / analysis_fps_limit; - Debug(1, "Recalculating motion_frame_skip (%d) = capture_fps(%f) / analysis_fps(%f)", - motion_frame_skip, capture_fps, analysis_fps_limit); - } - - if (snap->image) { - - // decoder may not have been able to provide an image - if (!ref_image.Buffer()) { - Debug(1, "Assigning instead of Detecting"); - ref_image.Assign(*(snap->image)); - alarm_image.Assign(*(snap->image)); - } else if (!(analysis_image_count % (motion_frame_skip+1))) { - Event::StringSet zoneSet; - Debug(1, "Detecting motion on image %d, image %p", snap->image_index, snap->image); - // Get new score. - snap->score = DetectMotion(*(snap->image), zoneSet); - - if (!snap->analysis_image) - snap->analysis_image = new Image(*(snap->image)); - // lets construct alarm cause. It will contain cause + names of zones alarmed - snap->zone_stats.reserve(zones.size()); - for (const Zone &zone : zones) { - const ZoneStats &stats = zone.GetStats(); - stats.DumpToLog("After detect motion"); - snap->zone_stats.push_back(stats); - if (zone.Alarmed()) { - if (!snap->alarm_cause.empty()) snap->alarm_cause += ","; - snap->alarm_cause += std::string(zone.Label()); - if (zone.AlarmImage()) - snap->analysis_image->Overlay(*(zone.AlarmImage())); + if (shared_data->capturing && !event) { + event = openEvent(snap, cause.empty() ? "Continuous" : cause, noteSetMap); + if (!event) { + event = openEvent(snap, cause, noteSetMap); + Info("%s: %03d - Opening new event %" PRIu64 ", alarm start", name.c_str(), analysis_image_count, event->Id()); + } // end if no event, so start it + shared_data->state = state = ALARM; + if (alarm_frame_count) { + Debug(1, "alarm frame count so SavePreAlarmFrames"); + event->SavePreAlarmFrames(); } + } else if (state != PREALARM) { + Info("%s: %03d - Gone into prealarm state", name.c_str(), analysis_image_count); + shared_data->state = state = PREALARM; } - alarm_image.Assign(*(snap->analysis_image)); - Debug(3, "After motion detection, score:%d last_motion_score(%d), new motion score(%d)", - score, last_motion_score, snap->score); - motion_frame_count += 1; - last_motion_score = snap->score; - - if (snap->score) { - if (cause.length()) cause += ", "; - cause += MOTION_CAUSE+std::string(":")+snap->alarm_cause; - noteSetMap[MOTION_CAUSE] = zoneSet; - } // end if motion_score + } else if (state == ALERT) { + alert_to_alarm_frame_count--; + Info("%s: %03d - Alarmed frame while in alert state. Consecutive alarmed frames left to return to alarm state: %03d", + name.c_str(), analysis_image_count, alert_to_alarm_frame_count); + if (alert_to_alarm_frame_count == 0) { + Info("%s: %03d - Gone back into alarm state", name.c_str(), analysis_image_count); + shared_data->state = state = ALARM; + } + } else if (state == TAPE) { + // Already recording, but IDLE so switch to ALARM + shared_data->state = state = ALARM; + Debug(1, "Was in TAPE, going into ALARM"); } else { - Debug(1, "Skipped motion detection last motion score was %d", last_motion_score); - alarm_image.Assign(*(snap->image)); + Debug(1, "Staying in %s", State_Strings[state].c_str()); } - } else { - Debug(1, "no image so skipping motion detection"); - } // end if has image -//score += last_motion_score; - } else { - Debug(1, "Not analysing %d", shared_data->analysing); - } // end if active and doing motion detection + if (state == ALARM) { + last_alarm_count = analysis_image_count; + } // This is needed so post_event_count counts after last alarmed frames while in ALARM not single alarmed frames while ALERT + } else if (!score and (snap->score == 0)) { // snap->score means -1 which means didn't do motion detection so don't do state transition + 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 - if (shared_data->recording) { - // If doing record, check to see if we need to close the event or not. - if (event && (section_length >= Seconds(min_section_length)) && (snap->timestamp - event->StartTime() > section_length)) { - if ( - (recording == RECORDING_ONMOTION && event_close_mode != CLOSE_TIME) - || (recording == RECORDING_ALWAYS && event_close_mode == CLOSE_TIME) - || 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, - 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()), - static_cast(std::chrono::duration_cast(snap->timestamp - event->StartTime()).count()), - static_cast(Seconds(section_length).count())); - closeEvent(); - } // end if section_length - } // end if event - - if (shared_data->capturing && !event) { - event = openEvent(snap, cause.empty() ? "Continuous" : cause, noteSetMap); - - Info("%s: %03d - Opened new event %" PRIu64 ", continuous section start", - name.c_str(), analysis_image_count, event->Id()); - /* To prevent cancelling out an existing alert\prealarm\alarm state */ - if (state == IDLE) { - shared_data->state = state = TAPE; - } - } // end if ! event - } // end if RECORDING - - if ((snap->score > 0) and (function != MONITOR)) { - if ((state == IDLE) || (state == TAPE) || (state == PREALARM)) { - // If we should end then previous continuous event and start a new non-continuous event - if (event) { - if (event->Frames() - && !event->AlarmFrames() - && (event_close_mode == CLOSE_ALARM) - && ((snap->timestamp - event->StartTime()) >= 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()); - closeEvent(); - } else { - // 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 - Debug(3, - "pre_alarm_count in event %d of %d, event frames %d, alarm frames %d event length %" PRIi64 " >=? %" PRIi64 " min close mode is ALARM? %d", - Event::PreAlarmCount(), pre_event_count, - event->Frames(), - event->AlarmFrames(), - static_cast(std::chrono::duration_cast(snap->timestamp - event->StartTime()).count()), - static_cast(Seconds(min_section_length).count()), - (event_close_mode == CLOSE_ALARM)); - } - } // end if event - - if (Event::PreAlarmCount() >= alarm_frame_count-1) { - shared_data->state = state = ALARM; - - 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()); - - if (!event and (recording != RECORDING_NONE)) { - event = openEvent(snap, cause, noteSetMap); - Info("%s: %03d - Opening new event %" PRIu64 ", alarm start", name.c_str(), analysis_image_count, event->Id()); - } // end if no event, so start it - shared_data->state = state = ALARM; - if (alarm_frame_count) { - Debug(1, "alarm frame count so SavePreAlarmFrames"); - event->SavePreAlarmFrames(); - } - } else if (state != PREALARM) { - Info("%s: %03d - Gone into prealarm state", name.c_str(), analysis_image_count); - shared_data->state = state = PREALARM; - } - } else if (state == ALERT) { - alert_to_alarm_frame_count--; - Info("%s: %03d - Alarmed frame while in alert state. Consecutive alarmed frames left to return to alarm state: %03d", - name.c_str(), analysis_image_count, alert_to_alarm_frame_count); - if (alert_to_alarm_frame_count == 0) { - Info("%s: %03d - Gone back into alarm state", name.c_str(), analysis_image_count); - shared_data->state = state = ALARM; - } - } else if (state == TAPE) { - // Already recording, but IDLE so switch to ALARM - shared_data->state = state = ALARM; - Debug(1, "Was in TAPE, going into ALARM"); - } else { - Debug(3, "Staying in %s", State_Strings[state].c_str()); - } - - if (state == ALARM) { - last_alarm_count = analysis_image_count; - } // This is needed so post_event_count counts after last alarmed frames while in ALARM not single alarmed frames while ALERT - } else if (!score and snap->score == 0) { // snap->score means -1 which means didn't do motion detection so don't do state transition - Debug(1, "!score"); - alert_to_alarm_frame_count = alarm_frame_count; // load same value configured for alarm_frame_count - - if (state == ALARM) { - Info("%s: %03d - Gone into alert state", name.c_str(), analysis_image_count); - shared_data->state = state = ALERT; - } else if (state == ALERT) { - if (((analysis_image_count - last_alarm_count) > post_event_count) - && - ((snap->timestamp - event->StartTime()) >= min_section_length)) { - Info("%s: %03d - Left alarm state (%" PRIu64 ") - %d(%d) images", - name.c_str(), analysis_image_count, event->Id(), event->Frames(), event->AlarmFrames()); - //if ( function != MOCORD || event_close_mode == CLOSE_ALARM || event->Cause() == SIGNAL_CAUSE ) - if (event) { - if ((recording == RECORDING_ONMOTION) + if (state == ALARM) { + Info("%s: %03d - Gone into alert state", name.c_str(), analysis_image_count); + shared_data->state = state = ALERT; + } else if (state == ALERT) { + if ( + ((analysis_image_count - last_alarm_count) > post_event_count) + && + (event->Duration() >= min_section_length)) { + Info("%s: %03d - Left alarm state (%" PRIu64 ") - %d(%d) images", + name.c_str(), analysis_image_count, event->Id(), event->Frames(), event->AlarmFrames()); + if ( + (recording = RECORDING_ONDEMAND) || (event_close_mode == CLOSE_ALARM || event_close_mode==CLOSE_IDLE) - ) { + ) { shared_data->state = state = IDLE; Info("%s: %03d - Closing event %" PRIu64 ", alarm end%s", - name.c_str(), analysis_image_count, event->Id(), (function==MOCORD)?", section truncated":"" ); + name.c_str(), analysis_image_count, event->Id(), (recording==RECORDING_ALWAYS)?", section truncated":"" ); + closeEvent(); } else { shared_data->state = state = TAPE; } - } else { - shared_data->state = state = IDLE; } + } else if (state == PREALARM) { + // Back to IDLE + shared_data->state = state = ((recording == RECORDING_ALWAYS) ? IDLE : TAPE); + } else { + Debug(1, + "State %d %s because analysis_image_count(%d)-last_alarm_count(%d) > post_event_count(%d) and timestamp.tv_sec(%" PRIi64 ") - recording.tv_src(%" PRIi64 ") >= min_section_length(%" PRIi64 ")", + state, + State_Strings[state].c_str(), + analysis_image_count, + last_alarm_count, + post_event_count, + static_cast(std::chrono::duration_cast(snap->timestamp.time_since_epoch()).count()), + static_cast(std::chrono::duration_cast(GetVideoWriterStartTime().time_since_epoch()).count()), + static_cast(Seconds(min_section_length).count())); } - } else if (state == PREALARM) { - // Back to IDLE - shared_data->state = state = (event ? TAPE : IDLE); - } else { - Debug(1, - "State %d %s because analysis_image_count(%d)-last_alarm_count(%d) > post_event_count(%d) and timestamp.tv_sec(%" PRIi64 ") - recording.tv_src(%" PRIi64 ") >= min_section_length(%" PRIi64 ")", - state, - State_Strings[state].c_str(), - analysis_image_count, - last_alarm_count, - post_event_count, - static_cast(std::chrono::duration_cast(snap->timestamp.time_since_epoch()).count()), - static_cast(std::chrono::duration_cast(GetVideoWriterStartTime().time_since_epoch()).count()), - static_cast(Seconds(min_section_length).count())); - } - if (Event::PreAlarmCount()) - Event::EmptyPreAlarmFrames(); - } // end if score or not + if (Event::PreAlarmCount()) + Event::EmptyPreAlarmFrames(); + } // end if score or not - if (score > snap->score) - snap->score = score; + if (score > snap->score) + snap->score = score; - if (state == PREALARM) { - // incremement pre alarm image count - Event::AddPreAlarmFrame(snap->image, snap->timestamp, score, nullptr); - } else if (state == ALARM) { - if (event) { - if (noteSetMap.size() > 0) + if (state == PREALARM) { + // incremement pre alarm image count + Event::AddPreAlarmFrame(snap->image, snap->timestamp, score, nullptr); + } else if (state == ALARM) { + if (event) { + if (noteSetMap.size() > 0) + event->updateNotes(noteSetMap); + if (section_length >= Seconds(min_section_length) && (event->Duration() >= section_length)) { + Warning("%s: %03d - event %" PRIu64 ", has exceeded desired section length. %" PRIi64 " - %" PRIi64 " = %" PRIi64 " >= %" PRIi64, + name.c_str(), analysis_image_count, 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()), + static_cast(std::chrono::duration_cast(event->Duration()).count()), + static_cast(Seconds(section_length).count())); + closeEvent(); + event = openEvent(snap, cause, noteSetMap); + } + } else { + Error("ALARM but no event"); + } + } else if (state == ALERT) { + // Alert means this frame has no motion, but we were alarmed and are still recording. + if ((noteSetMap.size() > 0) and event) event->updateNotes(noteSetMap); - if (section_length != Seconds(min_section_length) && (snap->timestamp - event->StartTime() >= section_length)) { - Warning("%s: %03d - event %" PRIu64 ", has exceeded desired section length. %" PRIi64 " - %" PRIi64 " = %" PRIi64 " >= %" PRIi64, - name.c_str(), analysis_image_count, event->Id(), - static_cast(std::chrono::duration_cast(snap->timestamp.time_since_epoch()).count()), - static_cast(std::chrono::duration_cast(GetVideoWriterStartTime().time_since_epoch()).count()), - static_cast(std::chrono::duration_cast(snap->timestamp - GetVideoWriterStartTime()).count()), - static_cast(Seconds(section_length).count())); - closeEvent(); - event = openEvent(snap, cause, noteSetMap); + } else if (state == TAPE) { + // bulk frame code moved to event. + } // end if state machine + + if (shared_data->recording) { + // If doing record, check to see if we need to close the event or not. + if (event && (section_length >= Seconds(min_section_length)) && (event->Duration() > section_length)) { + if ( + (recording == RECORDING_ONMOTION && event_close_mode != CLOSE_TIME) + || (recording == RECORDING_ALWAYS && event_close_mode == CLOSE_TIME) + || 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, + 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()), + static_cast(std::chrono::duration_cast(snap->timestamp - event->StartTime()).count()), + static_cast(Seconds(section_length).count())); + closeEvent(); + } // end if section_length + } // end if event + + if (!event) { + event = openEvent(snap, cause.empty() ? "Continuous" : cause, noteSetMap); + + Info("%s: %03d - Opened new event %" PRIu64 ", continuous section start", + name.c_str(), analysis_image_count, event->Id()); + /* To prevent cancelling out an existing alert\prealarm\alarm state */ + // This ignores current score status. This should all come after the state machine calculations + if (state == IDLE) { + shared_data->state = state = TAPE; + } + } // end if ! event + } // end if RECORDING + + if ((analysiing == ANALYSING_ALWAYS) and snap->image) { + if (!ref_image.Buffer()) { + Debug(1, "Assigning"); + ref_image.Assign(*(snap->image)); + } else { + Debug(1, "Blending"); + ref_image.Blend(*(snap->image), ( state==ALARM ? alarm_ref_blend_perc : ref_blend_perc )); + Debug(1, "Done Blending"); } - } else { - Error("ALARM but no event"); } - } else if (state == ALERT) { - // Alert means this frame has no motion, but we were alarmed and are still recording. - if ((noteSetMap.size() > 0) and event) - event->updateNotes(noteSetMap); - } else if (state == TAPE) { - // bulk frame code moved to event. - } // end if state machine + last_signal = signal; + } // end if videostream + } // end if signal + shared_data->last_frame_score = score; + } else { + Debug(3, "trigger == off"); + if (event) { + Info("%s: %03d - Closing event %" PRIu64 ", trigger off", name.c_str(), analysis_image_count, event->Id()); + closeEvent(); + } + shared_data->state = state = IDLE; + } // end if ( trigger_data->trigger_state != TRIGGER_OFF ) - if ((analysing != ANALYSING_NONE) and snap->image) { - if (!ref_image.Buffer()) { - Debug(1, "Assigning"); - ref_image.Assign(*(snap->image)); - } else { - Debug(1, "Blending"); - ref_image.Blend(*(snap->image), ( state==ALARM ? alarm_ref_blend_perc : ref_blend_perc )); - Debug(1, "Done Blending"); - } - } - last_signal = signal; - } // end if videostream - } // end if signal - shared_data->last_frame_score = score; - } else { - Debug(3, "trigger == off"); - if (event) { - Info("%s: %03d - Closing event %" PRIu64 ", trigger off", name.c_str(), analysis_image_count, event->Id()); - closeEvent(); - } - shared_data->state = state = IDLE; - } // end if ( trigger_data->trigger_state != TRIGGER_OFF ) - - if (event) event->AddPacket(snap); + if (event) event->AddPacket(snap); + } // end scope for event_lock // In the case where people have pre-alarm frames, the web ui will generate the frame images // from the mp4. So no one will notice anyways. if (snap->image and (videowriter == PASSTHROUGH)) { - if (!savejpegs) { - Debug(1, "Deleting image data for %d", snap->image_index); - // Don't need raw images anymore - delete snap->image; - snap->image = nullptr; - } - if (snap->analysis_image and !(savejpegs & 2)) { - Debug(1, "Deleting analysis image data for %d", snap->image_index); - delete snap->analysis_image; - snap->analysis_image = nullptr; - } + if (!savejpegs) { + Debug(1, "Deleting image data for %d", snap->image_index); + // Don't need raw images anymore + delete snap->image; + snap->image = nullptr; + } + if (snap->analysis_image and !(savejpegs & 2)) { + Debug(1, "Deleting analysis image data for %d", snap->image_index); + delete snap->analysis_image; + snap->analysis_image = nullptr; + } } packetqueue.clearPackets(snap); @@ -2586,7 +2580,8 @@ bool Monitor::Decode() { std::shared_ptr packet = packet_lock->packet_; if (packet->codec_type != AVMEDIA_TYPE_VIDEO) { Debug(4, "Not video"); - packetqueue.unlock(packet_lock); + //packetqueue.unlock(packet_lock); + delete packet_lock; return true; // Don't need decode } @@ -2692,24 +2687,35 @@ bool Monitor::Decode() { capture_image->Deinterlace_Blend(); } else if (deinterlacing_value == 4) { while (!zm_terminate) { + // ICON FIXME SHould we not clone decoder_it? ZMLockedPacket *deinterlace_packet_lock = packetqueue.get_packet(decoder_it); if (!deinterlace_packet_lock) { - packetqueue.unlock(packet_lock); + delete packet_lock; + //packetqueue.unlock(packet_lock); return false; } if (deinterlace_packet_lock->packet_->codec_type == packet->codec_type) { - capture_image->Deinterlace_4Field(deinterlace_packet_lock->packet_->image, (deinterlacing>>8)&0xff); - packetqueue.unlock(deinterlace_packet_lock); + if (!deinterlace_packet_lock->packet_->image) { + Error("Can't de-interlace when we have to decode. De-Interlacing should only be useful on old low res local cams"); + } else { + capture_image->Deinterlace_4Field(deinterlace_packet_lock->packet_->image, (deinterlacing>>8)&0xff); + } + delete deinterlace_packet_lock; + //packetqueue.unlock(deinterlace_packet_lock); break; } - packetqueue.unlock(deinterlace_packet_lock); + delete deinterlace_packet_lock; + //packetqueue.unlock(deinterlace_packet_lock); packetqueue.increment_it(decoder_it); } - if (zm_terminate) return false; + if (zm_terminate) { + delete packet_lock; + return false; + } } else if (deinterlacing_value == 5) { capture_image->Deinterlace_Blend_CustomRatio((deinterlacing>>8)&0xff); } - } + } // end if deinterlacing_value if (orientation != ROTATE_0) { Debug(3, "Doing rotation"); @@ -3140,7 +3146,9 @@ int Monitor::PrimeCapture() { #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) { + if (!Poller) { Poller = zm::make_unique(this); } else { @@ -3202,6 +3210,11 @@ int Monitor::Close() { soap = nullptr; } //End ONVIF #endif +#if HAVE_LIBCURL //Janus Teardown + if (janus_enabled && (purpose == CAPTURE)) { + remove_from_janus(); + } +#endif packetqueue.clear(); if (audio_fifo) { @@ -3338,3 +3351,141 @@ int SOAP_ENV__Fault(struct soap *soap, char *faultcode, char *faultstring, char return soap_send_empty_response(soap, SOAP_OK); } #endif + +size_t Monitor::WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) +{ + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; +} + +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 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; + //parse username and password + pos = path.find(":", 7); + if (pos == std::string::npos) return -1; + rtsp_username = path.substr(7, pos-7); + + pos2 = path.find("@", pos); + if (pos2 == std::string::npos) return -1; + + 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; + + //Assemble our actual request + postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; + postData += "\"request\" : \"create\", \"admin_key\" : \"supersecret\", \"type\" : \"rtsp\", "; + postData += "\"url\" : \""; + postData += rtsp_path; + postData += "\", \"rtsp_user\" : \""; + postData += rtsp_username; + postData += "\", \"rtsp_pwd\" : \""; + postData += rtsp_password; + postData += "\", \"id\" : "; + postData += std::to_string(id); + postData += ", \"video\" : true}}"; + + 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,"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 + std::string response; + std::string 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 += 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\" : \"destroy\", \"admin_key\" : \"supersecret\", \"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, "Removed stream from Janus: %s", response.c_str()); + curl_easy_cleanup(curl); + return 0; +} diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 60b30136b..dc3f56701 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -34,6 +34,7 @@ #include #include #include +#include #ifdef WITH_GSOAP #include "soapPullPointSubscriptionBindingProxy.h" @@ -47,6 +48,7 @@ class Group; #define MOTION_CAUSE "Motion" #define LINKED_CAUSE "Linked" + // // This is the main class for monitors. Each monitor is associated // with a camera and is effectively a collector for events. @@ -305,6 +307,7 @@ protected: RecordingSourceOption recording_source; // Primary, Secondary, Both bool decoding_enabled; // Whether the monitor will decode h264/h265 packets + bool janus_enabled; // Whether we set the h264/h265 stream up on janus std::string protocol; std::string method; @@ -485,6 +488,13 @@ protected: void set_credentials(struct soap *soap); #endif + //curl stuff for Janus + CURL *curl; + //helper class for CURL + static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp); + int add_to_janus(); + int remove_from_janus(); + // Used in check signal uint8_t red_val; uint8_t green_val; diff --git a/web/ajax/modals/function.php b/web/ajax/modals/function.php index 08b470840..b23bbb649 100644 --- a/web/ajax/modals/function.php +++ b/web/ajax/modals/function.php @@ -69,6 +69,16 @@ if ( !canEdit('Monitors') ) return; } ?> + +
+ + +'.$OLANG['FUNCTION_JANUS_ENABLED']['Help'].'
'; + } +?> + + + diff --git a/web/skins/classic/views/js/cycle.js b/web/skins/classic/views/js/cycle.js index 767fad475..670671faa 100644 --- a/web/skins/classic/views/js/cycle.js +++ b/web/skins/classic/views/js/cycle.js @@ -1,3 +1,6 @@ +var server; +var janus = null; +var streaming2; var intervalId; var pauseBtn = $j('#pauseBtn'); var playBtn = $j('#playBtn'); @@ -46,6 +49,69 @@ function initCycle() { intervalId = setInterval(nextCycleView, cycleRefreshTimeout); var scale = $j('#scale').val(); if ( scale == '0' || scale == 'auto' ) changeScale(); + + if (monitorData[monIdx].janusEnabled) { + server = "http://" + window.location.hostname + ":8088/janus"; + opaqueId = "streamingtest-"+Janus.randomString(12); + Janus.init({debug: "all", callback: function() { + janus = new Janus({ + server: server, + success: function() { + janus.attach({ + plugin: "janus.plugin.streaming", + opaqueId: opaqueId, + success: function(pluginHandle) { + streaming2 = pluginHandle; + var body = {"request": "watch", "id": monitorData[monIdx].id}; + streaming2.send({"message": body}); + }, + error: function(error) { + Janus.error(" -- Error attaching plugin... ", error); + }, + onmessage: function(msg, jsep) { + Janus.debug(" ::: Got a message :::"); + Janus.debug(msg); + var result = msg["result"]; + if (result !== null && result !== undefined) { + if (result["status"] !== undefined && result["status"] !== null) { + const status = result["status"]; + console.log(status); + } + } else if (msg["error"] !== undefined && msg["error"] !== null) { + Janus.debug(msg["error"]); + return; + } + if (jsep !== undefined && jsep !== null) { + Janus.debug("Handling SDP as well..."); + Janus.debug(jsep); + // Offer from the plugin, let's answer + streaming2.createAnswer({ + jsep: jsep, + // We want recvonly audio/video and, if negotiated, datachannels + media: {audioSend: false, videoSend: false, data: true}, + success: function(jsep) { + Janus.debug("Got SDP!"); + Janus.debug(jsep); + var body = {"request": "start"}; + streaming2.send({"message": body, "jsep": jsep}); + }, + error: function(error) { + Janus.error("WebRTC error:", error); + } + }); + } + }, //onmessage function + onremotestream: function(stream) { + Janus.debug(" ::: Got a remote track :::"); + Janus.debug(stream); + Janus.attachMediaStream(document.getElementById("liveStream" + monitorData[monIdx].id), stream); + document.getElementById("liveStream" + monitorData[monIdx].id).play(); + } + });// attach + } //Success functio + }); //new Janus + }}); //janus.init callback + } //if janus } function changeSize() { diff --git a/web/skins/classic/views/js/cycle.js.php b/web/skins/classic/views/js/cycle.js.php index 7bcd4497c..201deea76 100644 --- a/web/skins/classic/views/js/cycle.js.php +++ b/web/skins/classic/views/js/cycle.js.php @@ -20,7 +20,8 @@ monitorData[monitorData.length] = { 'url': 'UrlToIndex() ?>', 'onclick': function(){window.location.assign( '?view=watch&mid=Id() ?>' );}, 'type': 'Type() ?>', - 'refresh': 'Refresh() ?>' + 'refresh': 'Refresh() ?>', + 'janusEnabled': JanusEnabled() ?> }; 0 ) { - setInterval(reloadWebSite, interval*1000, i); + if (monitorData[i].janusEnabled) { + initJanus = true; + janusMonitors.push(monitorData[i]); + } + } + if (initJanus) { + server = "http://" + window.location.hostname + ":8088/janus"; + opaqueId = "streamingtest-"+Janus.randomString(12); + Janus.init({debug: "all", callback: function() { + janus = new Janus({ + server: server, + success: function() { + for ( var i = 0, length = janusMonitors.length; i < length; i++ ) { + attachVideo(janus, i); + } + } + }); + }}); + } + for ( var i = 0, length = monitorData.length; i < length; i++ ) { + if (!monitorData[i].janusEnabled) { + monitors[i] = new MonitorStream(monitorData[i]); + + // Start the fps and status updates. give a random delay so that we don't assault the server + var delay = Math.round( (Math.random()+0.5)*statusRefreshTimeout ); + console.log("delay: " + delay); + monitors[i].start(delay); + + var interval = monitors[i].refresh; + if ( monitors[i].type == 'WebSite' && interval > 0 ) { + setInterval(reloadWebSite, interval*1000, i); + } + monitors[i].setup_onclick(); } - monitors[i].setup_onclick(); } selectLayout('#zmMontageLayout'); } @@ -322,5 +351,59 @@ function watchFullscreen() { const content = document.getElementById('content'); openFullscreen(content); } + +function attachVideo(janus, i) { + janus.attach({ + plugin: "janus.plugin.streaming", + opaqueId: "streamingtest-"+Janus.randomString(12), + success: function(pluginHandle) { + janusMonitors[i].streaming = pluginHandle; + var body = {"request": "watch", "id": parseInt(janusMonitors[i].id)}; + janusMonitors[i].streaming.send({"message": body}); + }, + error: function(error) { + Janus.error(" -- Error attaching plugin... ", error); + }, + onmessage: function(msg, jsep) { + Janus.debug(" ::: Got a message :::"); + Janus.debug(msg); + var result = msg["result"]; + if (result !== null && result !== undefined) { + if (result["status"] !== undefined && result["status"] !== null) { + const status = result["status"]; + console.log(status); + } + } else if (msg["error"] !== undefined && msg["error"] !== null) { + Janus.error(msg["error"]); + return; + } + if (jsep !== undefined && jsep !== null) { + Janus.debug("Handling SDP as well..."); + Janus.debug(jsep); + // Offer from the plugin, let's answer + janusMonitors[i].streaming.createAnswer({ + jsep: jsep, + // We want recvonly audio/video and, if negotiated, datachannels + media: {audioSend: false, videoSend: false, data: true}, + success: function(jsep) { + Janus.debug("Got SDP!"); + Janus.debug(jsep); + var body = {"request": "start"}; + janusMonitors[i].streaming.send({"message": body, "jsep": jsep}); + }, + error: function(error) { + Janus.error("WebRTC error:", error); + } + }); + } + }, //onmessage function + onremotestream: function(ourstream) { + Janus.debug(" ::: Got a remote track :::"); + Janus.debug(ourstream); + Janus.attachMediaStream(document.getElementById("liveStream" + janusMonitors[i].id), ourstream); + document.getElementById("liveStream" + janusMonitors[i].id).play(); + } + });// attach +} // Kick everything off $j(document).ready(initPage); diff --git a/web/skins/classic/views/js/montage.js.php b/web/skins/classic/views/js/montage.js.php index 1c1c4130e..fac9e4d84 100644 --- a/web/skins/classic/views/js/montage.js.php +++ b/web/skins/classic/views/js/montage.js.php @@ -24,6 +24,7 @@ monitorData[monitorData.length] = { 'connKey': connKey() ?>, 'width': ViewWidth() ?>, 'height':ViewHeight() ?>, + 'janusEnabled':JanusEnabled() ?>, 'url': 'UrlToIndex( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>', 'onclick': function(){window.location.assign( '?view=watch&mid=Id() ?>' );}, 'type': 'Type() ?>', diff --git a/web/skins/classic/views/js/watch.js b/web/skins/classic/views/js/watch.js index 3a48581a0..2fcbaa548 100644 --- a/web/skins/classic/views/js/watch.js +++ b/web/skins/classic/views/js/watch.js @@ -9,6 +9,10 @@ var forceAlmBtn = $j('#forceAlmBtn'); var table = $j('#eventList'); var filterQuery = '&filter[Query][terms][0][attr]=MonitorId&filter[Query][terms][0][op]=%3d&filter[Query][terms][0][val]='+monitorId; +var server; +var janus = null; +var opaqueId; +var streaming2; /* This is the format of the json object sent by bootstrap-table @@ -899,15 +903,77 @@ function initPage() { } if (monitorType != 'WebSite') { - if (streamMode == 'single') { - statusCmdTimer = setTimeout(statusCmdQuery, 200); - setInterval(watchdogCheck, statusRefreshTimeout*2, 'status'); - } else { - streamCmdTimer = setTimeout(streamCmdQuery, 200); - setInterval(watchdogCheck, statusRefreshTimeout*2, 'stream'); + if (streamMode != 'janus') { + if (streamMode == 'single') { + statusCmdTimer = setTimeout(statusCmdQuery, 200); + setInterval(watchdogCheck, statusRefreshTimeout*2, 'status'); + } else { + streamCmdTimer = setTimeout(streamCmdQuery, 200); + setInterval(watchdogCheck, statusRefreshTimeout*2, 'stream'); + } } - - if (canStreamNative || (streamMode == 'single')) { + if (streamMode == 'janus') { + server = "http://" + window.location.hostname + ":8088/janus"; + opaqueId = "streamingtest-"+Janus.randomString(12); + Janus.init({debug: "all", callback: function() { + janus = new Janus({ + server: server, + success: function() { + janus.attach({ + plugin: "janus.plugin.streaming", + opaqueId: opaqueId, + success: function(pluginHandle) { + streaming2 = pluginHandle; + var body = {"request": "watch", "id": monitorId}; + streaming2.send({"message": body}); + }, + error: function(error) { + Janus.error(" -- Error attaching plugin... ", error); + }, + onmessage: function(msg, jsep) { + Janus.debug(" ::: Got a message :::"); + Janus.debug(msg); + var result = msg["result"]; + if (result !== null && result !== undefined) { + if (result["status"] !== undefined && result["status"] !== null) { + var status = result["status"]; + console.log(status); + } + } else if (msg["error"] !== undefined && msg["error"] !== null) { + Janus.error(msg["error"]); + return; + } + if (jsep !== undefined && jsep !== null) { + Janus.debug("Handling SDP as well..."); + Janus.debug(jsep); + // Offer from the plugin, let's answer + streaming2.createAnswer({ + jsep: jsep, + // We want recvonly audio/video and, if negotiated, datachannels + media: {audioSend: false, videoSend: false, data: true}, + success: function(jsep) { + Janus.debug("Got SDP!"); + Janus.debug(jsep); + var body = {"request": "start"}; + streaming2.send({"message": body, "jsep": jsep}); + }, + error: function(error) { + Janus.error("WebRTC error:", error); + } + }); + } + }, //onmessage function + onremotestream: function(stream) { + Janus.debug(" ::: Got a remote track :::"); + Janus.debug(stream); + Janus.attachMediaStream(document.getElementById("liveStream" + monitorId), stream); + document.getElementById("liveStream" + monitorId).play(); + } + }); // attach + } //Success functio + }); //new Janus + }}); //janus.init callback + } else if (canStreamNative || (streamMode == 'single')) { var streamImg = $j('#imageFeed img'); if (!streamImg) streamImg = $j('#imageFeed object'); if (!streamImg) { diff --git a/web/skins/classic/views/js/watch.js.php b/web/skins/classic/views/js/watch.js.php index be8065d12..2f383ab24 100644 --- a/web/skins/classic/views/js/watch.js.php +++ b/web/skins/classic/views/js/watch.js.php @@ -66,6 +66,7 @@ monitorData[monitorData.length] = { 'id': Id() ?>, 'width': ViewWidth() ?>, 'height':ViewHeight() ?>, + 'janusEnabled':JanusEnabled() ?>, 'url': 'UrlToIndex() ?>', 'onclick': function(){window.location.assign( '?view=watch&mid=Id() ?>' );}, 'type': 'Type() ?>', diff --git a/web/skins/classic/views/monitor.php b/web/skins/classic/views/monitor.php index ecd3606d4..0b951abe3 100644 --- a/web/skins/classic/views/monitor.php +++ b/web/skins/classic/views/monitor.php @@ -1279,6 +1279,16 @@ echo htmlSelect('newMonitor[ReturnLocation]', $return_options, $monitor->ReturnL + + + JanusEnabled() ? ' checked="checked"' : '' ?>/> +'.$OLANG['FUNCTION_JANUS_ENABLED']['Help'].''; + } +?> + + DefaultRate()); ?> diff --git a/web/skins/classic/views/montage.php b/web/skins/classic/views/montage.php index deaddcaf3..ed1869fab 100644 --- a/web/skins/classic/views/montage.php +++ b/web/skins/classic/views/montage.php @@ -325,5 +325,7 @@ foreach (array_reverse($zones) as $zone) { + + diff --git a/web/skins/classic/views/watch.php b/web/skins/classic/views/watch.php index 6acce1b5c..3128f7767 100644 --- a/web/skins/classic/views/watch.php +++ b/web/skins/classic/views/watch.php @@ -135,7 +135,11 @@ if (isset($_REQUEST['height'])) { } $connkey = generateConnKey(); -$streamMode = getStreamMode(); +if ( $monitor->JanusEnabled() ) { + $streamMode = 'janus'; +} else { + $streamMode = getStreamMode(); +} noCacheHeaders(); xhtmlHeaders(__FILE__, $monitor->Name().' - '.translate('Feed')); @@ -407,4 +411,6 @@ if ( ZM_WEB_SOUND_ON_ALARM ) { ?> + +