mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2025-12-23 22:37:53 -05:00
Adds RTSP2Web support for live view
This commit is contained in:
@@ -507,6 +507,8 @@ CREATE TABLE `Monitors` (
|
||||
`Enabled` tinyint(3) unsigned NOT NULL default '1',
|
||||
`DecodingEnabled` tinyint(3) unsigned NOT NULL default '1',
|
||||
`Decoding` enum('None','Ondemand','KeyFrames','KeyFrames+Ondemand', 'Always') NOT NULL default 'Always',
|
||||
`RTSP2WebEnabled` BOOLEAN NOT NULL default false,
|
||||
`RTSP2WebType` enum('HLS','MSE','WebRTC') NOT NULL default 'WebRTC',
|
||||
`JanusEnabled` BOOLEAN NOT NULL default false,
|
||||
`JanusAudioEnabled` BOOLEAN NOT NULL default false,
|
||||
`Janus_Profile_Override` VARCHAR(30) NOT NULL DEFAULT '',
|
||||
|
||||
33
db/zm_update-1.37.42.sql
Normal file
33
db/zm_update-1.37.42.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
--
|
||||
-- Update Monitors table to have RTSP2Web
|
||||
--
|
||||
|
||||
SELECT 'Checking for RTSP2WebEnabled in Monitors';
|
||||
SET @s = (SELECT IF(
|
||||
(SELECT COUNT(*)
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE table_name = 'Monitors'
|
||||
AND table_schema = DATABASE()
|
||||
AND column_name = 'RTSP2WebEnabled'
|
||||
) > 0,
|
||||
"SELECT 'Column RTSP2WebEnabled already exists on Monitors'",
|
||||
"ALTER TABLE `Monitors` ADD COLUMN `RTSP2WebEnabled` BOOLEAN NOT NULL default false AFTER `Decoding`"
|
||||
));
|
||||
|
||||
PREPARE stmt FROM @s;
|
||||
EXECUTE stmt;
|
||||
|
||||
SELECT 'Checking for RTSP2WebType in Monitors';
|
||||
SET @s = (SELECT IF(
|
||||
(SELECT COUNT(*)
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE table_name = 'Monitors'
|
||||
AND table_schema = DATABASE()
|
||||
AND column_name = 'RTSP2WebType'
|
||||
) > 0,
|
||||
"SELECT 'Column RTSP2WebType already exists on Monitors'",
|
||||
"ALTER TABLE `Monitors` ADD COLUMN `RTSP2WebType` enum('HLS','MSE','WebRTC') NOT NULL default 'WebRTC' AFTER `RTSP2WebEnabled`"
|
||||
));
|
||||
|
||||
PREPARE stmt FROM @s;
|
||||
EXECUTE stmt;
|
||||
@@ -433,6 +433,18 @@ our @options = (
|
||||
type => $types{boolean},
|
||||
category => 'system',
|
||||
},
|
||||
{
|
||||
name => 'ZM_RTSP2WEB_PATH',
|
||||
default => '',
|
||||
description => 'URL for RTSP2Web port.',
|
||||
help => q`This value points to the location of the RTSP2Web
|
||||
install, including username and password. If left blank, this
|
||||
will default to demo:demo@127.0.0.1:8083
|
||||
port 8083.
|
||||
`,
|
||||
type => $types{string},
|
||||
category => 'system',
|
||||
},
|
||||
{
|
||||
name => 'ZM_JANUS_SECRET',
|
||||
default => '',
|
||||
|
||||
@@ -36,6 +36,7 @@ set(ZM_BIN_SRC_FILES
|
||||
zm_local_camera.cpp
|
||||
zm_monitor.cpp
|
||||
zm_monitor_monitorlink.cpp
|
||||
zm_monitor_rtsp2web.cpp
|
||||
zm_monitor_janus.cpp
|
||||
zm_monitor_amcrest.cpp
|
||||
zm_monitorlink_expression.cpp
|
||||
|
||||
@@ -82,7 +82,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`, `Deleted`, `ServerId`, `StorageId`, `Type`, `Capturing`+0, `Analysing`+0, `AnalysisSource`+0, `AnalysisImage`+0,"
|
||||
"`Recording`+0, `RecordingSource`+0, `Decoding`+0, "
|
||||
"`Recording`+0, `RecordingSource`+0, `Decoding`+0, `RTSP2WebEnabled`, `RTSP2WebType`,"
|
||||
"`JanusEnabled`, `JanusAudioEnabled`, `Janus_Profile_Override`, `Janus_Use_RTSP_Restream`, `Janus_RTSP_User`, `Janus_RTSP_Session_Timeout`, "
|
||||
"`LinkedMonitors`, `EventStartCommand`, `EventEndCommand`, `AnalysisFPSLimit`, `AnalysisUpdateDelay`, `MaxFPS`, `AlarmMaxFPS`,"
|
||||
"`Device`, `Channel`, `Format`, `V4LMultiBuffer`, `V4LCapturesPerFrame`, " // V4L Settings
|
||||
@@ -155,6 +155,12 @@ std::string Decoding_Strings[] = {
|
||||
"Always"
|
||||
};
|
||||
|
||||
std::string RTSP2Web_Strings[] = {
|
||||
"HLS",
|
||||
"MSE",
|
||||
"WebRTC"
|
||||
};
|
||||
|
||||
std::string TriggerState_Strings[] = {
|
||||
"Cancel", "On", "Off"
|
||||
};
|
||||
@@ -169,6 +175,8 @@ Monitor::Monitor()
|
||||
analysing(ANALYSING_ALWAYS),
|
||||
recording(RECORDING_ALWAYS),
|
||||
decoding(DECODING_ALWAYS),
|
||||
RTSP2Web_enabled(false),
|
||||
RTSP2Web_type(WEBRTC),
|
||||
janus_enabled(false),
|
||||
janus_audio_enabled(false),
|
||||
janus_profile_override(""),
|
||||
@@ -301,6 +309,7 @@ Monitor::Monitor()
|
||||
Poll_Trigger_State(false),
|
||||
Event_Poller_Healthy(false),
|
||||
Event_Poller_Closes_Event(false),
|
||||
RTSP2Web_Manager(nullptr),
|
||||
Janus_Manager(nullptr),
|
||||
Amcrest_Manager(nullptr),
|
||||
#ifdef WITH_GSOAP
|
||||
@@ -330,7 +339,7 @@ Monitor::Monitor()
|
||||
/*
|
||||
std::string load_monitor_sql =
|
||||
"SELECT `Id`, `Name`, `Deleted`, `ServerId`, `StorageId`, `Type`, `Capturing`+0, `Analysing`+0, `AnalysisSource`+0, `AnalysisImage`+0,"
|
||||
"`Recording`+0, `RecordingSource`+0, `Decoding`+0, JanusEnabled, JanusAudioEnabled, Janus_Profile_Override, Janus_Use_RTSP_Restream, Janus_RTSP_User, Janus_RTSP_Session_Timeout, "
|
||||
"`Recording`+0, `RecordingSource`+0, `Decoding`+0, RTSP2WebEnabled, RTSP2WebType, JanusEnabled, JanusAudioEnabled, Janus_Profile_Override, Janus_Use_RTSP_Restream, Janus_RTSP_User, Janus_RTSP_Session_Timeout, "
|
||||
"LinkedMonitors, `EventStartCommand`, `EventEndCommand`, "
|
||||
"AnalysisFPSLimit, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS,"
|
||||
"Device, Channel, Format, V4LMultiBuffer, V4LCapturesPerFrame, " // V4L Settings
|
||||
@@ -390,6 +399,8 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) {
|
||||
|
||||
decoding = (DecodingOption)atoi(dbrow[col]); col++;
|
||||
// See below after save_jpegs for a recalculation of decoding_enabled
|
||||
RTSP2Web_enabled = dbrow[col] ? atoi(dbrow[col]) : false; col++;
|
||||
RTSP2Web_type = (RTSP2WebOption)atoi(dbrow[col]); col++;
|
||||
janus_enabled = dbrow[col] ? atoi(dbrow[col]) : false; col++;
|
||||
janus_audio_enabled = dbrow[col] ? atoi(dbrow[col]) : false; col++;
|
||||
janus_profile_override = std::string(dbrow[col] ? dbrow[col] : ""); col++;
|
||||
@@ -1015,6 +1026,9 @@ bool Monitor::connect() {
|
||||
|
||||
ReloadLinkedMonitors();
|
||||
|
||||
if (RTSP2Web_enabled) {
|
||||
RTSP2Web_Manager = new RTSP2WebManager(this);
|
||||
}
|
||||
if (janus_enabled) {
|
||||
Janus_Manager = new JanusManager(this);
|
||||
}
|
||||
@@ -1887,6 +1901,13 @@ bool Monitor::Poll() {
|
||||
#endif
|
||||
} // end if Amcrest or not
|
||||
} // end if Healthy
|
||||
Debug(1, "Trying to check RTSP2Web in Poller");
|
||||
if (RTSP2Web_enabled and RTSP2Web_Manager) {
|
||||
Debug(1, "Trying to add stream to RTSP2Web");
|
||||
if (RTSP2Web_Manager->check_RTSP2Web() == 0) {
|
||||
RTSP2Web_Manager->add_to_RTSP2Web();
|
||||
}
|
||||
}
|
||||
if (janus_enabled and Janus_Manager) {
|
||||
if (Janus_Manager->check_janus() == 0) {
|
||||
Janus_Manager->add_to_janus();
|
||||
@@ -3331,7 +3352,7 @@ int Monitor::PrimeCapture() {
|
||||
} // end if rtsp_server
|
||||
|
||||
//Poller Thread
|
||||
if (onvif_event_listener || janus_enabled || use_Amcrest_API) {
|
||||
if (onvif_event_listener || janus_enabled || RTSP2Web_enabled ||use_Amcrest_API) {
|
||||
if (!Poller) {
|
||||
Debug(1, "Creating unique poller thread");
|
||||
Poller = zm::make_unique<PollThread>(this);
|
||||
@@ -3411,6 +3432,12 @@ int Monitor::Close() {
|
||||
soap = nullptr;
|
||||
} //End ONVIF
|
||||
#endif
|
||||
//RTSP2Web teardoen
|
||||
if (RTSP2Web_enabled and (purpose == CAPTURE) and RTSP2Web_Manager) {
|
||||
delete RTSP2Web_Manager;
|
||||
RTSP2Web_Manager = nullptr;
|
||||
}
|
||||
|
||||
//Janus Teardown
|
||||
if (janus_enabled and (purpose == CAPTURE) and Janus_Manager) {
|
||||
delete Janus_Manager;
|
||||
|
||||
@@ -111,6 +111,12 @@ public:
|
||||
DECODING_ALWAYS
|
||||
} DecodingOption;
|
||||
|
||||
typedef enum {
|
||||
HLS,
|
||||
MSE,
|
||||
WEBRTC
|
||||
} RTSP2WebOption;
|
||||
|
||||
typedef enum {
|
||||
LOCAL=1,
|
||||
REMOTE,
|
||||
@@ -332,6 +338,28 @@ protected:
|
||||
int start_Amcrest();
|
||||
};
|
||||
|
||||
class RTSP2WebManager {
|
||||
protected:
|
||||
Monitor *parent;
|
||||
CURL *curl = nullptr;
|
||||
//helper class for CURL
|
||||
static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp);
|
||||
bool RTSP2Web_Healthy;
|
||||
bool Use_RTSP_Restream;
|
||||
std::string RTSP2Web_endpoint;
|
||||
std::string rtsp_username;
|
||||
std::string rtsp_password;
|
||||
std::string rtsp_path;
|
||||
|
||||
public:
|
||||
explicit RTSP2WebManager(Monitor *parent_);
|
||||
~RTSP2WebManager();
|
||||
void load_from_monitor();
|
||||
int add_to_RTSP2Web();
|
||||
int check_RTSP2Web();
|
||||
int remove_from_RTSP2Web();
|
||||
};
|
||||
|
||||
class JanusManager {
|
||||
protected:
|
||||
Monitor *parent;
|
||||
@@ -379,6 +407,8 @@ protected:
|
||||
RecordingSourceOption recording_source; // Primary, Secondary, Both
|
||||
|
||||
DecodingOption decoding; // Whether the monitor will decode h264/h265 packets
|
||||
bool RTSP2Web_enabled; // Whether we set the h264/h265 stream up on RTSP2Web
|
||||
int RTSP2Web_type; // Whether we set the h264/h265 stream up on RTSP2Web
|
||||
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 janus_profile_override; // The Profile-ID to force the stream to use.
|
||||
@@ -575,6 +605,7 @@ protected:
|
||||
bool Event_Poller_Healthy;
|
||||
bool Event_Poller_Closes_Event;
|
||||
|
||||
RTSP2WebManager *RTSP2Web_Manager;
|
||||
JanusManager *Janus_Manager;
|
||||
AmcrestAPI *Amcrest_Manager;
|
||||
|
||||
|
||||
185
src/zm_monitor_rtsp2web.cpp
Normal file
185
src/zm_monitor_rtsp2web.cpp
Normal file
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// ZoneMinder Monitor::RTSP2WebManager Class Implementation, $Date$, $Revision$
|
||||
// Copyright (C) 2022 Jonathan Bennett
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
//
|
||||
|
||||
#include "zm_crypt.h"
|
||||
#include "zm_monitor.h"
|
||||
#include "zm_server.h"
|
||||
#include "zm_time.h"
|
||||
#include <regex>
|
||||
|
||||
std::string remove_newlines(std::string input);
|
||||
std::string escape_json_string(std::string input);
|
||||
|
||||
Monitor::RTSP2WebManager::RTSP2WebManager(Monitor *parent_) :
|
||||
parent(parent_),
|
||||
RTSP2Web_Healthy(false)
|
||||
{
|
||||
Use_RTSP_Restream = false;
|
||||
if ((config.rtsp2web_path != nullptr) && (config.rtsp2web_path[0] != '\0')) {
|
||||
RTSP2Web_endpoint = config.rtsp2web_path;
|
||||
//remove the trailing slash if present
|
||||
if (RTSP2Web_endpoint.back() == '/') RTSP2Web_endpoint.pop_back();
|
||||
} else {
|
||||
RTSP2Web_endpoint = "demo:demo@127.0.0.1:8083";
|
||||
}
|
||||
|
||||
rtsp_path = parent->path;
|
||||
if (!parent->user.empty()) {
|
||||
rtsp_username = escape_json_string(parent->user);
|
||||
rtsp_password = escape_json_string(parent->pass);
|
||||
if (rtsp_path.find("rtsp://") == 0) {
|
||||
rtsp_path = "rtsp://" + rtsp_username + ":" + rtsp_password + "@" + rtsp_path.substr(7, std::string::npos);
|
||||
} else {
|
||||
rtsp_path = "rtsp://" + rtsp_username + ":" + rtsp_password + "@" + rtsp_path;
|
||||
}
|
||||
}
|
||||
Debug(1, "Monitor %u rtsp url is %s", parent->id, rtsp_path.c_str());
|
||||
}
|
||||
|
||||
Monitor::RTSP2WebManager::~RTSP2WebManager() {
|
||||
remove_from_RTSP2Web();
|
||||
}
|
||||
|
||||
int Monitor::RTSP2WebManager::check_RTSP2Web() {
|
||||
|
||||
curl = curl_easy_init();
|
||||
if (!curl) return -1;
|
||||
|
||||
//Assemble our actual request
|
||||
std::string response;
|
||||
std::string endpoint = RTSP2Web_endpoint+"/stream/"+std::to_string(parent->id)+"/info";
|
||||
curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
Warning("Attempted to send to %s and got %s", endpoint.c_str(), curl_easy_strerror(res));
|
||||
return -1;
|
||||
}
|
||||
|
||||
Debug(1, "Queried for stream status: %s", remove_newlines(response).c_str());
|
||||
if (response.find("\"status\": 0") != std::string::npos) {
|
||||
if (response.find("stream not found") != std::string::npos) {
|
||||
Warning("Mountpoint Missing");
|
||||
return 0;
|
||||
} else {
|
||||
Warning("unknown RTSP2Web error");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int Monitor::RTSP2WebManager::add_to_RTSP2Web() {
|
||||
Debug(1, "Adding stream to RTSP2Web");
|
||||
curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
Error("Failed to init curl");
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string endpoint = RTSP2Web_endpoint+"/stream/"+std::to_string(parent->id)+"/add";
|
||||
|
||||
//Assemble our actual request
|
||||
std::string postData = "{\"name\" : \"";
|
||||
postData += std::string(parent->Name());
|
||||
postData += "\", \"channels\" : { \"0\" : {";
|
||||
postData += "\"name\" : \"ch1\", \"url\" : \"";
|
||||
postData += rtsp_path;
|
||||
postData += "\", \"on_demand\": true, \"debug\": false, \"status\": 0}}}";
|
||||
|
||||
Debug(1, "Sending %s to %s", postData.c_str(), endpoint.c_str());
|
||||
|
||||
CURLcode res;
|
||||
std::string response;
|
||||
|
||||
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);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
Error("Failed to curl_easy_perform adding rtsp stream");
|
||||
return -1;
|
||||
}
|
||||
|
||||
Debug(1, "Adding stream response: %s", remove_newlines(response).c_str());
|
||||
//scan for missing session or handle id "No such session" "no such handle"
|
||||
if (response.find("\"status\": 1") != std::string::npos) {
|
||||
Warning("RTSP2Web failed adding stream");
|
||||
return -2;
|
||||
}
|
||||
|
||||
Debug(1,"Added stream to RTSP2Web: %s", response.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Monitor::RTSP2WebManager::remove_from_RTSP2Web() {
|
||||
|
||||
curl = curl_easy_init();
|
||||
if (!curl) return -1;
|
||||
|
||||
std::string endpoint = RTSP2Web_endpoint+"/stream/"+std::to_string(parent->id)+"/delete";
|
||||
std::string response;
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
if (res != CURLE_OK) {
|
||||
Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res));
|
||||
} else {
|
||||
Debug(1, "Removed stream from RTSP2Web: %s", remove_newlines(response).c_str());
|
||||
}
|
||||
|
||||
curl_easy_cleanup(curl);
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t Monitor::RTSP2WebManager::WriteCallback(void *contents, size_t size, size_t nmemb, void *userp)
|
||||
{
|
||||
((std::string*)userp)->append((char*)contents, size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
std::string remove_newlines( std::string str ) {
|
||||
while (!str.empty() && str.find("\n") != std::string::npos)
|
||||
str.erase(std::remove(str.begin(), str.end(), '\n'), str.cend());
|
||||
return str;
|
||||
}
|
||||
|
||||
/*
|
||||
std::string escape_json_string( std::string input ) {
|
||||
std::string tmp;
|
||||
tmp = regex_replace(input, std::regex("\n"), "\\n");
|
||||
tmp = regex_replace(tmp, std::regex("\b"), "\\b");
|
||||
tmp = regex_replace(tmp, std::regex("\f"), "\\f");
|
||||
tmp = regex_replace(tmp, std::regex("\r"), "\\r");
|
||||
tmp = regex_replace(tmp, std::regex("\t"), "\\t");
|
||||
tmp = regex_replace(tmp, std::regex("\""), "\\\"");
|
||||
tmp = regex_replace(tmp, std::regex("[\\\\]"), "\\\\");
|
||||
return tmp;
|
||||
}
|
||||
*/
|
||||
@@ -70,6 +70,16 @@ if ( !canEdit('Monitors') ) return;
|
||||
}
|
||||
?>
|
||||
|
||||
</div>
|
||||
<div class="form-group" id="FunctionRTSP2WebEnabled">
|
||||
<label for="newRTSP2WebEnabled"><?php echo translate('RTSP2Web Enabled') ?></label>
|
||||
<input type="checkbox" name="newRTSP2WebEnabled" id="newRTSP2WebEnabled" value="1"/>
|
||||
<?php
|
||||
if ( isset($OLANG['FUNCTION_RTSP2WEB_ENABLED']) ) {
|
||||
echo '<div class="form-text">'.$OLANG['FUNCTION_RTWP2WEB_ENABLED']['Help'].'</div>';
|
||||
}
|
||||
?>
|
||||
|
||||
</div>
|
||||
<div class="form-group" id="FunctionJanusEnabled">
|
||||
<label for="newJanusEnabled"><?php echo translate('Janus Enabled') ?></label>
|
||||
|
||||
@@ -145,6 +145,8 @@ public static function getStatuses() {
|
||||
'AnalysisImage' => 'FullColour',
|
||||
'Enabled' => array('type'=>'boolean','default'=>1),
|
||||
'Decoding' => 'Always',
|
||||
'RTSP2WebEnabled' => 'HLS',
|
||||
'RTSP2WebType' => array('type'=>'integer','default'=>0),
|
||||
'JanusEnabled' => array('type'=>'boolean','default'=>0),
|
||||
'JanusAudioEnabled' => array('type'=>'boolean','default'=>0),
|
||||
'Janus_Profile_Override' => '',
|
||||
@@ -1030,7 +1032,7 @@ public static function getStatuses() {
|
||||
'format' => ZM_MPEG_LIVE_FORMAT
|
||||
) );
|
||||
$html .= getVideoStreamHTML( 'liveStream'.$this->Id(), $streamSrc, $options['width'], $options['height'], ZM_MPEG_LIVE_FORMAT, $this->Name() );
|
||||
} else if ( $this->JanusEnabled() ) {
|
||||
} else if ( $this->JanusEnabled() or $this->RTSP2WebEnabled()) {
|
||||
$html .= '<video id="liveStream'.$this->Id().'" '.
|
||||
((isset($options['width']) and $options['width'] and $options['width'] != '0')?'width="'.$options['width'].'"':'').
|
||||
' autoplay muted controls playsinline=""></video>';
|
||||
|
||||
@@ -10,6 +10,11 @@ function MonitorStream(monitorData) {
|
||||
this.url_to_zms = monitorData.url_to_zms;
|
||||
this.width = monitorData.width;
|
||||
this.height = monitorData.height;
|
||||
this.RTSP2WebEnabled = monitorData.RTSP2WebEnabled;
|
||||
this.RTSP2WebType = monitorData.RTSP2WebType;
|
||||
this.mseStreamingStarted = false;
|
||||
this.mseQueue = [];
|
||||
this.mseSourceBuffer = null;
|
||||
this.janusEnabled = monitorData.janusEnabled;
|
||||
this.janusPin = monitorData.janus_pin;
|
||||
this.server_id = monitorData.server_id;
|
||||
@@ -202,33 +207,58 @@ function MonitorStream(monitorData) {
|
||||
attachVideo(parseInt(this.id), this.janusPin);
|
||||
this.statusCmdTimer = setTimeout(this.statusCmdQuery.bind(this), delay);
|
||||
return;
|
||||
} else if (this.RTSP2WebEnabled) {
|
||||
videoEl = document.getElementById("liveStream" + this.id);
|
||||
rtsp2webModUrl = ZM_RTSP2WEB_PATH.split('@')[1]; // drop the username and password for viewing
|
||||
if (this.RTSP2WebType == "HLS") {
|
||||
hlsUrl = "http://" + rtsp2webModUrl + "/stream/" + this.id + "/channel/0/hls/live/index.m3u8"
|
||||
if (Hls.isSupported()) {
|
||||
const hls = new Hls()
|
||||
hls.loadSource(hlsUrl)
|
||||
hls.attachMedia(videoEl)
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
videoEl.src = hlsUrl
|
||||
}
|
||||
} else if (this.RTSP2WebType == "MSE") {
|
||||
videoEl.addEventListener('pause', () => {
|
||||
if (videoEl.currentTime > videoEl.buffered.end(videoEl.buffered.length - 1)) {
|
||||
videoEl.currentTime = videoEl.buffered.end(videoEl.buffered.length - 1) - 0.1;
|
||||
videoEl.play();
|
||||
}
|
||||
});
|
||||
startMsePlay(this, videoEl, "ws://" + rtsp2webModUrl + "/stream/" + this.id + "/channel/0/mse?uuid=" + this.id + "&channel=0");
|
||||
} else if (this.RTSP2WebType == "WebRTC") {
|
||||
webrtcUrl = "http://" + rtsp2webModUrl + "/stream/" + this.id + "/channel/0/webrtc";
|
||||
console.log(webrtcUrl);
|
||||
startRTSP2WebRTSPPlay(videoEl, webrtcUrl);
|
||||
}
|
||||
} else {
|
||||
// zms stream
|
||||
const stream = this.getElement();
|
||||
if (!stream) return;
|
||||
if (!stream.src) {
|
||||
// Website Monitors won't have an img tag, neither will video
|
||||
console.log('No src for #liveStream'+this.id);
|
||||
console.log(stream);
|
||||
return;
|
||||
}
|
||||
this.streamCmdTimer = clearTimeout(this.streamCmdTimer);
|
||||
// Step 1 make sure we are streaming instead of a static image
|
||||
if (stream.getAttribute('loading') == 'lazy') {
|
||||
stream.setAttribute('loading', 'eager');
|
||||
}
|
||||
src = stream.src.replace(/mode=single/i, 'mode=jpeg');
|
||||
if (-1 == src.search('connkey')) {
|
||||
src += '&connkey='+this.connKey;
|
||||
}
|
||||
if (stream.src != src) {
|
||||
console.log("Setting to streaming: " + src);
|
||||
stream.src = '';
|
||||
stream.src = src;
|
||||
}
|
||||
stream.onerror = this.img_onerror.bind(this);
|
||||
stream.onload = this.img_onload.bind(this);
|
||||
}
|
||||
|
||||
// zms stream
|
||||
const stream = this.getElement();
|
||||
if (!stream) return;
|
||||
if (!stream.src) {
|
||||
// Website Monitors won't have an img tag, neither will video
|
||||
console.log('No src for #liveStream'+this.id);
|
||||
console.log(stream);
|
||||
return;
|
||||
}
|
||||
this.streamCmdTimer = clearTimeout(this.streamCmdTimer);
|
||||
// Step 1 make sure we are streaming instead of a static image
|
||||
if (stream.getAttribute('loading') == 'lazy') {
|
||||
stream.setAttribute('loading', 'eager');
|
||||
}
|
||||
src = stream.src.replace(/mode=single/i, 'mode=jpeg');
|
||||
if (-1 == src.search('connkey')) {
|
||||
src += '&connkey='+this.connKey;
|
||||
}
|
||||
if (stream.src != src) {
|
||||
console.log("Setting to streaming: " + src);
|
||||
stream.src = '';
|
||||
stream.src = src;
|
||||
}
|
||||
stream.onerror = this.img_onerror.bind(this);
|
||||
stream.onload = this.img_onload.bind(this);
|
||||
}; // this.start
|
||||
|
||||
this.stop = function() {
|
||||
@@ -821,3 +851,111 @@ const waitUntil = (condition) => {
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
function startRTSP2WebRTSPPlay(videoEl, url) {
|
||||
const webrtc = new RTCPeerConnection({
|
||||
iceServers: [{
|
||||
urls: ['stun:stun.l.google.com:19302']
|
||||
}],
|
||||
sdpSemantics: 'unified-plan'
|
||||
})
|
||||
webrtc.ontrack = function (event) {
|
||||
console.log(event.streams.length + ' track is delivered')
|
||||
videoEl.srcObject = event.streams[0]
|
||||
videoEl.play()
|
||||
}
|
||||
webrtc.addTransceiver('video', { direction: 'sendrecv' })
|
||||
webrtc.onnegotiationneeded = async function handleNegotiationNeeded () {
|
||||
const offer = await webrtc.createOffer()
|
||||
|
||||
await webrtc.setLocalDescription(offer)
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ data: btoa(webrtc.localDescription.sdp) })
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
try {
|
||||
webrtc.setRemoteDescription(
|
||||
new RTCSessionDescription({ type: 'answer', sdp: atob(data) })
|
||||
)
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const webrtcSendChannel = webrtc.createDataChannel('rtsptowebSendChannel')
|
||||
webrtcSendChannel.onopen = (event) => {
|
||||
console.log(`${webrtcSendChannel.label} has opened`)
|
||||
webrtcSendChannel.send('ping')
|
||||
}
|
||||
webrtcSendChannel.onclose = (_event) => {
|
||||
console.log(`${webrtcSendChannel.label} has closed`)
|
||||
startPlay(videoEl, url)
|
||||
}
|
||||
webrtcSendChannel.onmessage = event => console.log(event.data)
|
||||
}
|
||||
|
||||
|
||||
function startMsePlay (context, videoEl, url) {
|
||||
const mse = new MediaSource();
|
||||
mse.addEventListener('sourceopen', function () {
|
||||
const ws = new WebSocket(url);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = function (event) {
|
||||
console.log('Connect to ws');
|
||||
}
|
||||
ws.onmessage = function (event) {
|
||||
const data = new Uint8Array(event.data);
|
||||
if (data[0] === 9) {
|
||||
let mimeCodec;
|
||||
const decodedArr = data.slice(1);
|
||||
if (window.TextDecoder) {
|
||||
mimeCodec = new TextDecoder('utf-8').decode(decodedArr);
|
||||
} else {
|
||||
mimeCodec = Utf8ArrayToStr(decodedArr);
|
||||
}
|
||||
context.mseSourceBuffer = mse.addSourceBuffer('video/mp4; codecs="' + mimeCodec + '"');
|
||||
context.mseSourceBuffer.mode = 'segments';
|
||||
context.mseSourceBuffer.addEventListener('updateend', pushMsePacket, videoEl, context);
|
||||
} else {
|
||||
readMsePacket(event.data, videoEl, context);
|
||||
}
|
||||
}
|
||||
}, false)
|
||||
videoEl.src = window.URL.createObjectURL(mse);
|
||||
}
|
||||
|
||||
function pushMsePacket (videoEl, context) {
|
||||
//const videoEl = document.querySelector('#mse-video')
|
||||
let packet
|
||||
|
||||
if (context != undefined && !context.mseSourceBuffer.updating) {
|
||||
if (context.mseQueue.length > 0) {
|
||||
packet = context.mseQueue.shift()
|
||||
context.mseSourceBuffer.appendBuffer(packet)
|
||||
} else {
|
||||
context.mseStreamingStarted = false
|
||||
}
|
||||
}
|
||||
if (videoEl.buffered != undefined && videoEl.buffered.length > 0) {
|
||||
if (typeof document.hidden !== 'undefined' && document.hidden) {
|
||||
// no sound, browser paused video without sound in background
|
||||
videoEl.currentTime = videoEl.buffered.end((videoEl.buffered.length - 1)) - 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readMsePacket (packet, videoEl, context) {
|
||||
if (!context.mseStreamingStarted) {
|
||||
context.mseSourceBuffer.appendBuffer(packet)
|
||||
context.mseStreamingStarted = true
|
||||
return
|
||||
}
|
||||
context.mseQueue.push(packet)
|
||||
if (!context.mseSourceBuffer.updating) {
|
||||
pushMsePacket(videoEl, context)
|
||||
}
|
||||
}
|
||||
|
||||
2
web/js/hls.js
Normal file
2
web/js/hls.js
Normal file
File diff suppressed because one or more lines are too long
@@ -906,6 +906,16 @@ KeyFrames: Only keyframes will be decoded, so viewing frame rate will be very lo
|
||||
None: No frames will be decoded, live view and thumbnails will not be available~~~~
|
||||
'
|
||||
),
|
||||
'FUNCTION_RTSP2WEB_ENABLED' => array(
|
||||
'Help' => '
|
||||
Attempt to use RTSP2Web streaming server for h264/h265 live view. Experimental, but allows
|
||||
for significantly better performance.'
|
||||
),
|
||||
'FUNCTION_RTSP2WEB_TYPE' => array(
|
||||
'Help' => '
|
||||
RTSP2Web supports MSE (Media Source Extensions), HLS (HTTP Live Streaming), and WebRTC.
|
||||
Each has its advantages, with WebRTC probably being the most performant, but also the most picky about codecs.'
|
||||
),
|
||||
'FUNCTION_JANUS_ENABLED' => array(
|
||||
'Help' => '
|
||||
Attempt to use Janus streaming server for h264/h265 live view. Experimental, but allows
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
//
|
||||
|
||||
$RTSP2WebTypes = array(
|
||||
'HLS' => 'HLS',
|
||||
'MSE' => 'MSE',
|
||||
'WebRTC' => 'WebRTC',
|
||||
);
|
||||
$rates = array(
|
||||
-1600 => '-16x',
|
||||
-1000 => '-10x',
|
||||
|
||||
@@ -197,6 +197,13 @@ if ( $monitor->JanusEnabled() ) {
|
||||
<script src="/javascript/janus/janus.js"></script>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
if ( $monitor->RTSP2WebEnabled() and $monitor->RTSP2WebType == "HLS") {
|
||||
?>
|
||||
<script src="<?php echo cache_bust('js/hls.js') ?>"></script>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
<script src="<?php echo cache_bust('js/MonitorStream.js') ?>"></script>
|
||||
<?php xhtmlFooter() ?>
|
||||
|
||||
@@ -23,6 +23,8 @@ monitorData[monitorData.length] = {
|
||||
'onclick': function(){window.location.assign( '?view=watch&mid=<?php echo $monitor->Id() ?>' );},
|
||||
'type': '<?php echo $monitor->Type() ?>',
|
||||
'refresh': '<?php echo $monitor->Refresh() ?>',
|
||||
'RTSP2WebEnabled': <?php echo $monitor->RTSP2WebEnabled() ?>,
|
||||
'RTSP2WebType': '<?php echo $monitor->RTSP2WebType() ?>',
|
||||
'janusEnabled': <?php echo $monitor->JanusEnabled() ?>,
|
||||
'janus_pin': '<?php echo $monitor->Janus_Pin() ?>'
|
||||
};
|
||||
|
||||
@@ -22,6 +22,8 @@ monitorData[monitorData.length] = {
|
||||
'connKey': '<?php echo $monitor->connKey() ?>',
|
||||
'width': <?php echo $monitor->ViewWidth() ?>,
|
||||
'height':<?php echo $monitor->ViewHeight() ?>,
|
||||
'RTSP2WebEnabled':<?php echo $monitor->RTSP2WebEnabled() ?>,
|
||||
'RTSP2WebType':'<?php echo $monitor->RTSP2WebType() ?>',
|
||||
'janusEnabled':<?php echo $monitor->JanusEnabled() ?>,
|
||||
'url': '<?php echo $monitor->UrlToIndex( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>',
|
||||
'url_to_zms': '<?php echo $monitor->UrlToZMS( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>',
|
||||
|
||||
@@ -47,6 +47,8 @@ monitorData[monitorData.length] = {
|
||||
'connKey': <?php echo $m->connKey() ?>,
|
||||
'width': <?php echo $m->ViewWidth() ?>,
|
||||
'height':<?php echo $m->ViewHeight() ?>,
|
||||
'RTSP2WebEnabled':<?php echo $m->RTSP2WebEnabled() ?>,
|
||||
'RTSP2WebType':'<?php echo $m->RTSP2WebType() ?>',
|
||||
'janusEnabled':<?php echo $m->JanusEnabled() ?>,
|
||||
'url': '<?php echo $m->UrlToIndex() ?>',
|
||||
'onclick': function(){window.location.assign( '?view=watch&mid=<?php echo $m->Id() ?>' );},
|
||||
|
||||
@@ -1233,6 +1233,18 @@ echo htmlSelect('newMonitor[OutputContainer]', $videowriter_containers, $monitor
|
||||
<label><?php echo translate('RTSPStreamName'); echo makeHelpLink('OPTIONS_RTSPSTREAMNAME') ?></label>
|
||||
<input type="text" name="newMonitor[RTSPStreamName]" value="<?php echo validHtmlStr($monitor->RTSPStreamName()) ?>"/>
|
||||
</li>
|
||||
<li id="FunctionRTSP2WebEnabled">
|
||||
<label><?php echo translate('RTSP2Web Live Stream') ?></label>
|
||||
<input type="checkbox" name="newMonitor[RTSP2WebEnabled]" value="1"<?php echo $monitor->RTSP2WebEnabled() ? ' checked="checked"' : '' ?>/>
|
||||
<?php
|
||||
if ( isset($OLANG['FUNCTION_RTSP2WEB_ENABLED']) ) {
|
||||
echo '<div class="form-text">'.$OLANG['FUNCTION_RTSP2WEB_ENABLED']['Help'].'</div>';
|
||||
}
|
||||
?>
|
||||
<li>
|
||||
<label><?php echo translate('RTSP2Web Type') ?> <?php echo $monitor->RTSP2WebType() ?> </label>
|
||||
<?php echo htmlSelect('newMonitor[RTSP2WebType]', $RTSP2WebTypes, $monitor->RTSP2WebType()); ?>
|
||||
</li>
|
||||
<li id="FunctionJanusEnabled">
|
||||
<label><?php echo translate('Janus Live Stream') ?></label>
|
||||
<input type="checkbox" name="newMonitor[JanusEnabled]" value="1"<?php echo $monitor->JanusEnabled() ? ' checked="checked"' : '' ?>/>
|
||||
|
||||
@@ -113,6 +113,7 @@ include('_monitor_filters.php');
|
||||
$filterbar = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$need_hls = false;
|
||||
$need_janus = false;
|
||||
$monitors = array();
|
||||
foreach ($displayMonitors as &$row) {
|
||||
@@ -131,6 +132,10 @@ foreach ($displayMonitors as &$row) {
|
||||
$heights[$row['Height'].'px'] = $row['Height'].'px';
|
||||
}
|
||||
$monitor = $monitors[] = new ZM\Monitor($row);
|
||||
|
||||
if ( $monitor->RTSP2WebEnabled() and $monitor->RTSP2WebType == "HLS") {
|
||||
$need_hls = true;
|
||||
}
|
||||
if ($monitor->JanusEnabled()) {
|
||||
$need_janus = true;
|
||||
}
|
||||
@@ -270,6 +275,9 @@ foreach ($monitors as $monitor) {
|
||||
<script src="<?php echo cache_bust('js/adapter.min.js') ?>"></script>
|
||||
<?php if ($need_janus) { ?>
|
||||
<script src="/javascript/janus/janus.js"></script>
|
||||
<?php } ?>
|
||||
<?php if ($need_hls) { ?>
|
||||
<script src="<?php echo cache_bust('js/hls.js') ?>"></script>
|
||||
<?php } ?>
|
||||
<script src="<?php echo cache_bust('js/MonitorStream.js') ?>"></script>
|
||||
<?php xhtmlFooter() ?>
|
||||
|
||||
@@ -165,6 +165,8 @@ if (
|
||||
}
|
||||
if ($monitor->JanusEnabled()) {
|
||||
$streamMode = 'janus';
|
||||
} else if ($monitor->RTSP2WebEnabled()) {
|
||||
$streamMode = $monitor->RTSP2WebType();
|
||||
} else {
|
||||
$streamMode = getStreamMode();
|
||||
}
|
||||
@@ -417,6 +419,13 @@ if ( $monitor->JanusEnabled() ) {
|
||||
<script src="/javascript/janus/janus.js"></script>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
if ( $monitor->RTSP2WebEnabled() and $monitor->RTSP2WebType == "HLS") {
|
||||
?>
|
||||
<script src="<?php echo cache_bust('js/hls.js') ?>"></script>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
<script src="<?php echo cache_bust('js/MonitorStream.js') ?>"></script>
|
||||
<?php xhtmlFooter() ?>
|
||||
|
||||
Reference in New Issue
Block a user