Adds RTSP2Web support for live view

This commit is contained in:
Jonathan Bennett
2023-07-29 23:41:51 -05:00
parent 9295c390ca
commit 5c9588c012
20 changed files with 530 additions and 30 deletions

View File

@@ -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
View 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;

View File

@@ -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 => '',

View File

@@ -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

View File

@@ -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;

View File

@@ -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
View 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;
}
*/

View File

@@ -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>

View File

@@ -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>';

View File

@@ -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
View File

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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',

View File

@@ -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() ?>

View File

@@ -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() ?>'
};

View File

@@ -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) : '') ?>',

View File

@@ -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() ?>' );},

View File

@@ -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"' : '' ?>/>

View File

@@ -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() ?>

View File

@@ -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() ?>